Add logging of the log list timestamp

This CL adds the parsed log list timestamp from the JSON log list, as
well as logging a SUCCESS event (with timestamp and signature) when the
log list has been successfully updated.

Fixes: 391327942
Fixes: 394278886
Bug: 378626065
Flag: com.android.net.ct.flags.certificate_transparency_service
Test: atest NetworkSecurityUnitTests

Change-Id: I8f0d96ca96cd89dce5da588cb478f9332537df48
This commit is contained in:
Bessie Jiang 2025-02-07 11:01:43 +00:00
commit ae659ace56
7 changed files with 153 additions and 41 deletions

View file

@ -199,7 +199,6 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
}
LogListUpdateStatus updateStatus = mSignatureVerifier.verify(contentUri, metadataUri);
// TODO(b/391327942): parse file and log the timestamp of the log list
if (!updateStatus.isSignatureVerified()) {
Log.w(TAG, "Log list did not pass verification");
@ -209,42 +208,30 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
return;
}
boolean success = false;
try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
success = compatVersion.install(inputStream);
updateStatus = compatVersion.install(inputStream, updateStatus.toBuilder());
} catch (IOException e) {
Log.e(TAG, "Could not install new content", e);
return;
}
if (success) {
// Reset the number of consecutive log list failure updates back to zero.
mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
mDataStore.store();
} else {
mLogger.logCTLogListUpdateStateChangedEvent(
updateStatus
.toBuilder()
.setState(CTLogListUpdateState.VERSION_ALREADY_EXISTS)
.build());
}
}
mLogger.logCTLogListUpdateStateChangedEvent(updateStatus);
}
private void handleDownloadFailed(DownloadStatus status) {
Log.e(TAG, "Download failed with " + status);
LogListUpdateStatus.Builder updateStatus = LogListUpdateStatus.builder();
LogListUpdateStatus.Builder updateStatusBuilder = LogListUpdateStatus.builder();
if (status.isHttpError()) {
updateStatus
updateStatusBuilder
.setState(CTLogListUpdateState.HTTP_ERROR)
.setHttpErrorStatusCode(status.reason());
} else {
// TODO(b/384935059): handle blocked domain logging
updateStatus.setDownloadStatus(Optional.of(status.reason()));
updateStatusBuilder.setDownloadStatus(Optional.of(status.reason()));
}
mLogger.logCTLogListUpdateStateChangedEvent(updateStatus.build());
mLogger.logCTLogListUpdateStateChangedEvent(updateStatusBuilder.build());
}
private long download(String url) {

View file

@ -35,10 +35,12 @@ public interface CertificateTransparencyLogger {
enum CTLogListUpdateState {
UNKNOWN_STATE,
HTTP_ERROR,
LOG_LIST_INVALID,
PUBLIC_KEY_NOT_FOUND,
SIGNATURE_INVALID,
SIGNATURE_NOT_FOUND,
SIGNATURE_VERIFICATION_FAILED,
SUCCESS,
VERSION_ALREADY_EXISTS
}
}

View file

@ -20,6 +20,7 @@ import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFIC
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DEVICE_OFFLINE;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DOWNLOAD_CANNOT_RESUME;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_LOG_LIST_INVALID;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_NO_DISK_SPACE;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_INVALID;
@ -29,6 +30,7 @@ import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFIC
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__PENDING_WAITING_FOR_WIFI;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__SUCCESS;
import android.app.DownloadManager;
@ -43,6 +45,12 @@ class CertificateTransparencyLoggerImpl implements CertificateTransparencyLogger
@Override
public void logCTLogListUpdateStateChangedEvent(LogListUpdateStatus updateStatus) {
if (updateStatus.isSuccessful()) {
resetFailureCount();
} else {
updateFailureCount();
}
int updateState =
updateStatus
.downloadStatus()
@ -56,20 +64,31 @@ class CertificateTransparencyLoggerImpl implements CertificateTransparencyLogger
updateState,
failureCount,
updateStatus.httpErrorStatusCode(),
updateStatus.signature());
updateStatus.signature(),
updateStatus.logListTimestamp());
}
private void logCTLogListUpdateStateChangedEvent(
int updateState, int failureCount, int httpErrorStatusCode, String signature) {
updateFailureCount();
int updateState,
int failureCount,
int httpErrorStatusCode,
String signature,
long logListTimestamp) {
CertificateTransparencyStatsLog.write(
CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED,
updateState,
failureCount,
httpErrorStatusCode,
signature,
/* logListTimestampMs= */ 0);
logListTimestamp);
}
/**
* Resets the number of consecutive log list update failures in the data store back to zero.
*/
private void resetFailureCount() {
mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
mDataStore.store();
}
/**
@ -112,6 +131,8 @@ class CertificateTransparencyLoggerImpl implements CertificateTransparencyLogger
switch (updateState) {
case HTTP_ERROR:
return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
case LOG_LIST_INVALID:
return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_LOG_LIST_INVALID;
case PUBLIC_KEY_NOT_FOUND:
return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
case SIGNATURE_INVALID:
@ -120,6 +141,8 @@ class CertificateTransparencyLoggerImpl implements CertificateTransparencyLogger
return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_NOT_FOUND;
case SIGNATURE_VERIFICATION_FAILED:
return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_VERIFICATION;
case SUCCESS:
return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__SUCCESS;
case VERSION_ALREADY_EXISTS:
return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
case UNKNOWN_STATE:

View file

@ -23,6 +23,8 @@ import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
import org.json.JSONException;
import org.json.JSONObject;
@ -69,22 +71,29 @@ class CompatibilityVersion {
* Installs a log list within this compatibility version directory.
*
* @param newContent an input stream providing the log list
* @param statusBuilder status obj builder containing details of the log list update process
* @return true if the log list was installed successfully, false otherwise.
* @throws IOException if the list cannot be saved in the CT directory.
*/
boolean install(InputStream newContent) throws IOException {
LogListUpdateStatus install(
InputStream newContent, LogListUpdateStatus.Builder statusBuilder) throws IOException {
String content = new String(newContent.readAllBytes(), UTF_8);
try {
JSONObject contentJson = new JSONObject(content);
return install(
new ByteArrayInputStream(content.getBytes()),
new JSONObject(content).getString("version"));
contentJson.getString("version"),
statusBuilder.setLogListTimestamp(contentJson.getLong("log_list_timestamp")));
} catch (JSONException e) {
Log.e(TAG, "invalid log list format", e);
return false;
return statusBuilder.setState(CTLogListUpdateState.LOG_LIST_INVALID).build();
}
}
private boolean install(InputStream newContent, String version) throws IOException {
LogListUpdateStatus install(
InputStream newContent, String version, LogListUpdateStatus.Builder statusBuilder)
throws IOException {
// To support atomically replacing the old configuration directory with the new
// there's a bunch of steps. We create a new directory with the logs and then do
// an atomic update of the current symlink to point to the new directory.
@ -100,7 +109,7 @@ class CompatibilityVersion {
if (newLogsDir.getCanonicalPath().equals(mCurrentLogsDirSymlink.getCanonicalPath())) {
Log.i(TAG, newLogsDir + " already exists, skipping install.");
deleteOldLogDirectories();
return false;
return statusBuilder.setState(CTLogListUpdateState.VERSION_ALREADY_EXISTS).build();
}
// If the symlink has not been updated then the previous installation failed and
// this is a re-attempt. Clean-up leftover files and try again.
@ -134,7 +143,7 @@ class CompatibilityVersion {
// 7. Cleanup
Log.i(TAG, "New logs installed at " + newLogsDir);
deleteOldLogDirectories();
return true;
return statusBuilder.setState(CTLogListUpdateState.SUCCESS).build();
}
String getCompatVersion() {

View file

@ -19,6 +19,7 @@ import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListU
import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_INVALID;
import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_NOT_FOUND;
import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED;
import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SUCCESS;
import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
@ -52,6 +53,14 @@ public abstract class LogListUpdateStatus {
return signature() != null && signature().length() > 0;
}
boolean isSuccessful() {
return state() == SUCCESS;
}
static LogListUpdateStatus getDefaultInstance() {
return builder().build();
}
@AutoValue.Builder
abstract static class Builder {
abstract Builder setState(CTLogListUpdateState updateState);

View file

@ -84,6 +84,7 @@ public class CertificateTransparencyDownloaderTest {
private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
private long mNextDownloadId = 666;
private static final long LOG_LIST_TIMESTAMP = 123456789L;
@Before
public void setUp() throws IOException, GeneralSecurityException {
@ -426,7 +427,7 @@ public class CertificateTransparencyDownloaderTest {
verify(mLogger, times(1))
.logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.VERSION_ALREADY_EXISTS);
assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.LOG_LIST_INVALID);
assertThat(statusValue.signature())
.isEqualTo(new String(signatureBytes, StandardCharsets.UTF_8));
}
@ -466,7 +467,8 @@ public class CertificateTransparencyDownloaderTest {
}
@Test
public void testDownloader_endToEndSuccess_installNewVersion() throws Exception {
public void testDownloader_endToEndSuccess_installNewVersion_andLogsSuccess() throws Exception {
// Arrange
String newVersion = "456";
File logListFile = makeLogListFile(newVersion);
File metadataFile = sign(logListFile);
@ -474,6 +476,7 @@ public class CertificateTransparencyDownloaderTest {
assertNoVersionIsInstalled();
// Act
// 1. Start download of public key.
mCertificateTransparencyDownloader.startPublicKeyDownload();
@ -491,7 +494,19 @@ public class CertificateTransparencyDownloaderTest {
mCertificateTransparencyDownloader.onReceive(
mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
// Assert
assertInstallSuccessful(newVersion);
verify(mLogger, times(1))
.logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.SUCCESS);
assertThat(statusValue.signature())
.isEqualTo(
new String(
Base64.getDecoder().decode(toByteArray(metadataFile)),
StandardCharsets.UTF_8));
assertThat(statusValue.logListTimestamp()).isEqualTo(LOG_LIST_TIMESTAMP);
}
private void assertNoVersionIsInstalled() {
@ -600,7 +615,11 @@ public class CertificateTransparencyDownloaderTest {
File logListFile = File.createTempFile("log_list", "json");
try (OutputStream outputStream = new FileOutputStream(logListFile)) {
outputStream.write(new JSONObject().put("version", version).toString().getBytes(UTF_8));
JSONObject contentJson =
new JSONObject()
.put("version", version)
.put("log_list_timestamp", LOG_LIST_TIMESTAMP);
outputStream.write(contentJson.toString().getBytes(UTF_8));
}
return logListFile;

View file

@ -15,6 +15,11 @@
*/
package com.android.server.net.ct;
import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.LOG_LIST_INVALID;
import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SUCCESS;
import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.UNKNOWN_STATE;
import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.VERSION_ALREADY_EXISTS;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.platform.app.InstrumentationRegistry;
@ -37,6 +42,8 @@ import java.io.InputStream;
public class CompatibilityVersionTest {
private static final String TEST_VERSION = "v123";
private static final long LOG_LIST_TIMESTAMP = 123456789L;
private static final String SIGNATURE = "fake_signature";
private final File mTestDir =
InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
@ -52,6 +59,7 @@ public class CompatibilityVersionTest {
@Test
public void testCompatibilityVersion_versionDirectory_setupSuccessful() {
File versionDir = mCompatVersion.getVersionDir();
assertThat(versionDir.exists()).isFalse();
assertThat(versionDir.getAbsolutePath()).startsWith(mTestDir.getAbsolutePath());
assertThat(versionDir.getAbsolutePath()).endsWith(TEST_VERSION);
@ -60,6 +68,7 @@ public class CompatibilityVersionTest {
@Test
public void testCompatibilityVersion_symlink_setupSuccessful() {
File dirSymlink = mCompatVersion.getLogsDirSymlink();
assertThat(dirSymlink.exists()).isFalse();
assertThat(dirSymlink.getAbsolutePath())
.startsWith(mCompatVersion.getVersionDir().getAbsolutePath());
@ -68,18 +77,44 @@ public class CompatibilityVersionTest {
@Test
public void testCompatibilityVersion_logsFile_setupSuccessful() {
File logsFile = mCompatVersion.getLogsFile();
assertThat(logsFile.exists()).isFalse();
assertThat(logsFile.getAbsolutePath())
.startsWith(mCompatVersion.getLogsDirSymlink().getAbsolutePath());
}
@Test
public void testCompatibilityVersion_installSuccessful_keepsStatusDetails() throws Exception {
String version = "i_am_version";
JSONObject logList = makeLogList(version, "i_am_content");
try (InputStream inputStream = asStream(logList)) {
assertThat(
mCompatVersion.install(
inputStream,
LogListUpdateStatus.builder()
.setSignature(SIGNATURE)
.setState(UNKNOWN_STATE)))
.isEqualTo(
LogListUpdateStatus.builder()
.setSignature(SIGNATURE)
.setLogListTimestamp(LOG_LIST_TIMESTAMP)
// Ensure the state is correctly overridden to SUCCESS
.setState(SUCCESS)
.build());
}
}
@Test
public void testCompatibilityVersion_installSuccessful() throws Exception {
String version = "i_am_version";
JSONObject logList = makeLogList(version, "i_am_content");
try (InputStream inputStream = asStream(logList)) {
assertThat(mCompatVersion.install(inputStream)).isTrue();
assertThat(
mCompatVersion.install(
inputStream, LogListUpdateStatus.builder()))
.isEqualTo(getSuccessfulUpdateStatus());
}
File logListFile = mCompatVersion.getLogsFile();
@ -107,7 +142,10 @@ public class CompatibilityVersionTest {
@Test
public void testCompatibilityVersion_deleteSuccessfully() throws Exception {
try (InputStream inputStream = asStream(makeLogList(/* version= */ "123"))) {
assertThat(mCompatVersion.install(inputStream)).isTrue();
assertThat(
mCompatVersion.install(
inputStream, LogListUpdateStatus.builder()))
.isEqualTo(getSuccessfulUpdateStatus());
}
mCompatVersion.delete();
@ -118,7 +156,10 @@ public class CompatibilityVersionTest {
@Test
public void testCompatibilityVersion_invalidLogList() throws Exception {
try (InputStream inputStream = new ByteArrayInputStream(("not_a_valid_list".getBytes()))) {
assertThat(mCompatVersion.install(inputStream)).isFalse();
assertThat(
mCompatVersion.install(
inputStream, LogListUpdateStatus.builder()))
.isEqualTo(LogListUpdateStatus.builder().setState(LOG_LIST_INVALID).build());
}
assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
@ -138,7 +179,10 @@ public class CompatibilityVersionTest {
JSONObject newLogList = makeLogList(existingVersion, "i_am_the_real_content");
try (InputStream inputStream = asStream(newLogList)) {
assertThat(mCompatVersion.install(inputStream)).isTrue();
assertThat(
mCompatVersion.install(
inputStream, LogListUpdateStatus.builder()))
.isEqualTo(getSuccessfulUpdateStatus());
}
assertThat(readAsString(logsListFile)).isEqualTo(newLogList.toString());
@ -149,11 +193,21 @@ public class CompatibilityVersionTest {
String existingVersion = "666";
JSONObject existingLogList = makeLogList(existingVersion, "i_was_installed_successfully");
try (InputStream inputStream = asStream(existingLogList)) {
assertThat(mCompatVersion.install(inputStream)).isTrue();
assertThat(
mCompatVersion.install(
inputStream, LogListUpdateStatus.builder()))
.isEqualTo(getSuccessfulUpdateStatus());
}
try (InputStream inputStream = asStream(makeLogList(existingVersion, "i_am_ignored"))) {
assertThat(mCompatVersion.install(inputStream)).isFalse();
assertThat(
mCompatVersion.install(
inputStream, LogListUpdateStatus.builder()))
.isEqualTo(
LogListUpdateStatus.builder()
.setState(VERSION_ALREADY_EXISTS)
.setLogListTimestamp(LOG_LIST_TIMESTAMP)
.build());
}
assertThat(readAsString(mCompatVersion.getLogsFile()))
@ -165,13 +219,22 @@ public class CompatibilityVersionTest {
}
private static JSONObject makeLogList(String version) throws JSONException {
return new JSONObject().put("version", version);
return new JSONObject()
.put("version", version)
.put("log_list_timestamp", LOG_LIST_TIMESTAMP);
}
private static JSONObject makeLogList(String version, String content) throws JSONException {
return makeLogList(version).put("content", content);
}
private static LogListUpdateStatus getSuccessfulUpdateStatus() {
return LogListUpdateStatus.builder()
.setState(SUCCESS)
.setLogListTimestamp(LOG_LIST_TIMESTAMP)
.build();
}
private static String readAsString(File file) throws IOException {
try (InputStream in = new FileInputStream(file)) {
return new String(in.readAllBytes());