Implement killswitch mechanism for Android CT

Enabling the killswitch will scheduele the CT job and initialize the downloader.

Disabling the killswitch will remove the scheduled job, unregister the downloader and delete the compatibility version directory.

Also, remove the CertificateTransparencyFlagsListener as it is deprecated.

Bug: 384688420
Change-Id: I23d2201e22f305284ac47f521258f856b8a9efc2
Flag: com.android.net.ct.flags.certificate_transparency_service
Test: atest NetworkSecurityUnitTests
This commit is contained in:
Sandro Montanari 2024-12-27 08:07:10 -08:00
commit f24643a42a
4 changed files with 112 additions and 171 deletions

View file

@ -61,6 +61,8 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
private final CertificateTransparencyInstaller mInstaller;
private final CertificateTransparencyLogger mLogger;
private boolean started = false;
CertificateTransparencyDownloader(
Context context,
DataStore dataStore,
@ -76,15 +78,32 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
mLogger = logger;
}
void initialize() {
void start() {
if (started) {
return;
}
mInstaller.addCompatibilityVersion(Config.COMPATIBILITY_VERSION);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED);
mContext.registerReceiver(
this,
new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
Context.RECEIVER_EXPORTED);
started = true;
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyDownloader initialized successfully");
Log.d(TAG, "CertificateTransparencyDownloader started.");
}
}
void stop() {
if (!started) {
return;
}
mContext.unregisterReceiver(this);
mInstaller.removeCompatibilityVersion(Config.COMPATIBILITY_VERSION);
started = false;
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyDownloader stopped.");
}
}
@ -216,8 +235,9 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
String version = null;
try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
version = new JSONObject(new String(inputStream.readAllBytes(), UTF_8))
.getString("version");
version =
new JSONObject(new String(inputStream.readAllBytes(), UTF_8))
.getString("version");
} catch (JSONException | IOException e) {
Log.e(TAG, "Could not extract version from log list", e);
return;
@ -242,7 +262,7 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
mLogger.logCTLogListUpdateFailedEvent(
CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS,
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
}
}
}
@ -251,30 +271,28 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
Log.e(TAG, "Download failed with " + status);
if (updateFailureCount()) {
int failureCount = mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
int failureCount =
mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
// HTTP Error
if (400 <= status.reason() && status.reason() <= 600) {
mLogger.logCTLogListUpdateFailedEvent(
CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR,
failureCount,
status.reason()
);
status.reason());
} else {
// TODO(b/384935059): handle blocked domain logging
// TODO(b/384936292): add additionalchecks for pending wifi status
mLogger.logCTLogListUpdateFailedEvent(
downloadStatusToFailureReason(status.reason()),
failureCount
);
downloadStatusToFailureReason(status.reason()), failureCount);
}
}
}
/** Converts DownloadStatus reason into failure reason to log. */
private int downloadStatusToFailureReason(int downloadStatusReason) {
switch(downloadStatusReason) {
switch (downloadStatusReason) {
case DownloadManager.PAUSED_WAITING_TO_RETRY:
case DownloadManager.PAUSED_WAITING_FOR_NETWORK:
return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DEVICE_OFFLINE;
@ -300,8 +318,9 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
* @return whether the failure count exceeds the threshold and should be logged.
*/
private boolean updateFailureCount() {
int failure_count = mDataStore.getPropertyInt(
Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
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);
@ -309,8 +328,7 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
boolean shouldReport = new_failure_count >= Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD;
if (shouldReport) {
Log.d(TAG,
"Log list update failure count exceeds threshold: " + new_failure_count);
Log.d(TAG, "Log list update failure count exceeds threshold: " + new_failure_count);
}
return shouldReport;
}

View file

@ -1,128 +0,0 @@
/*
* 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;
import android.annotation.RequiresApi;
import android.os.Build;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.text.TextUtils;
import android.util.Log;
import java.security.GeneralSecurityException;
import java.util.concurrent.Executors;
/** Listener class for the Certificate Transparency Phenotype flags. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesChangedListener {
private static final String TAG = "CertificateTransparencyFlagsListener";
private final DataStore mDataStore;
private final SignatureVerifier mSignatureVerifier;
private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
CertificateTransparencyFlagsListener(
DataStore dataStore,
SignatureVerifier signatureVerifier,
CertificateTransparencyDownloader certificateTransparencyDownloader) {
mDataStore = dataStore;
mSignatureVerifier = signatureVerifier;
mCertificateTransparencyDownloader = certificateTransparencyDownloader;
}
void initialize() {
mDataStore.load();
mCertificateTransparencyDownloader.initialize();
DeviceConfig.addOnPropertiesChangedListener(
Config.NAMESPACE_NETWORK_SECURITY, Executors.newSingleThreadExecutor(), this);
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyFlagsListener initialized successfully");
}
// TODO: handle property changes triggering on boot before registering this listener.
}
@Override
public void onPropertiesChanged(Properties properties) {
if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
return;
}
String newPublicKey =
DeviceConfig.getString(
Config.NAMESPACE_NETWORK_SECURITY,
Config.FLAG_PUBLIC_KEY,
/* defaultValue= */ "");
String newVersion =
DeviceConfig.getString(
Config.NAMESPACE_NETWORK_SECURITY,
Config.FLAG_VERSION,
/* defaultValue= */ "");
String newContentUrl =
DeviceConfig.getString(
Config.NAMESPACE_NETWORK_SECURITY,
Config.FLAG_CONTENT_URL,
/* defaultValue= */ "");
String newMetadataUrl =
DeviceConfig.getString(
Config.NAMESPACE_NETWORK_SECURITY,
Config.FLAG_METADATA_URL,
/* defaultValue= */ "");
if (TextUtils.isEmpty(newPublicKey)
|| TextUtils.isEmpty(newVersion)
|| TextUtils.isEmpty(newContentUrl)
|| TextUtils.isEmpty(newMetadataUrl)) {
return;
}
if (Config.DEBUG) {
Log.d(TAG, "newPublicKey=" + newPublicKey);
Log.d(TAG, "newVersion=" + newVersion);
Log.d(TAG, "newContentUrl=" + newContentUrl);
Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
}
String oldVersion = mDataStore.getProperty(Config.VERSION);
String oldContentUrl = mDataStore.getProperty(Config.CONTENT_URL);
String oldMetadataUrl = mDataStore.getProperty(Config.METADATA_URL);
if (TextUtils.equals(newVersion, oldVersion)
&& TextUtils.equals(newContentUrl, oldContentUrl)
&& TextUtils.equals(newMetadataUrl, oldMetadataUrl)) {
Log.i(TAG, "No flag changed, ignoring update");
return;
}
try {
mSignatureVerifier.setPublicKey(newPublicKey);
} catch (GeneralSecurityException | IllegalArgumentException e) {
Log.e(TAG, "Error setting the public Key", e);
return;
}
// TODO: handle the case where there is already a pending download.
mDataStore.setProperty(Config.CONTENT_URL, newContentUrl);
mDataStore.setProperty(Config.METADATA_URL, newMetadataUrl);
mDataStore.store();
if (mCertificateTransparencyDownloader.startMetadataDownload() == -1) {
Log.e(TAG, "Metadata download not started.");
} else if (Config.DEBUG) {
Log.d(TAG, "Metadata download started successfully.");
}
}
}

View file

@ -37,6 +37,7 @@ public class CertificateTransparencyJob extends BroadcastReceiver {
private final DataStore mDataStore;
private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
private final AlarmManager mAlarmManager;
private final PendingIntent mPendingIntent;
private boolean mDependenciesReady = false;
@ -49,9 +50,15 @@ public class CertificateTransparencyJob extends BroadcastReceiver {
mDataStore = dataStore;
mCertificateTransparencyDownloader = certificateTransparencyDownloader;
mAlarmManager = context.getSystemService(AlarmManager.class);
mPendingIntent =
PendingIntent.getBroadcast(
mContext,
/* requestCode= */ 0,
new Intent(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
PendingIntent.FLAG_IMMUTABLE);
}
void initialize() {
void schedule() {
mContext.registerReceiver(
this,
new IntentFilter(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
@ -60,14 +67,21 @@ public class CertificateTransparencyJob extends BroadcastReceiver {
AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime(), // schedule first job at earliest convenient time.
AlarmManager.INTERVAL_DAY,
PendingIntent.getBroadcast(
mContext,
0,
new Intent(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
PendingIntent.FLAG_IMMUTABLE));
mPendingIntent);
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyJob scheduled successfully.");
Log.d(TAG, "CertificateTransparencyJob scheduled.");
}
}
void cancel() {
mContext.unregisterReceiver(this);
mAlarmManager.cancel(mPendingIntent);
mCertificateTransparencyDownloader.stop();
mDependenciesReady = false;
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyJob canceled.");
}
}
@ -82,7 +96,7 @@ public class CertificateTransparencyJob extends BroadcastReceiver {
}
if (!mDependenciesReady) {
mDataStore.load();
mCertificateTransparencyDownloader.initialize();
mCertificateTransparencyDownloader.start();
mDependenciesReady = true;
}

View file

@ -25,27 +25,29 @@ import android.annotation.RequiresApi;
import android.content.Context;
import android.net.ct.ICertificateTransparencyManager;
import android.os.Build;
import android.util.Log;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import com.android.server.SystemService;
import java.util.concurrent.Executors;
/** Implementation of the Certificate Transparency service. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub
implements DeviceConfig.OnPropertiesChangedListener {
private static final String TAG = "CertificateTransparencyService";
private final CertificateTransparencyFlagsListener mFlagsListener;
private final CertificateTransparencyJob mCertificateTransparencyJob;
private boolean started = false;
/**
* @return true if the CertificateTransparency service is enabled.
*/
public static boolean enabled(Context context) {
return DeviceConfig.getBoolean(
Config.NAMESPACE_NETWORK_SECURITY,
Config.FLAG_SERVICE_ENABLED,
/* defaultValue= */ true)
&& certificateTransparencyService()
&& certificateTransparencyConfiguration();
return certificateTransparencyService() && certificateTransparencyConfiguration();
}
/** Creates a new {@link CertificateTransparencyService} object. */
@ -61,8 +63,6 @@ public class CertificateTransparencyService extends ICertificateTransparencyMana
signatureVerifier,
new CertificateTransparencyInstaller(),
new CertificateTransparencyLogger());
mFlagsListener =
new CertificateTransparencyFlagsListener(dataStore, signatureVerifier, downloader);
mCertificateTransparencyJob =
new CertificateTransparencyJob(context, dataStore, downloader);
}
@ -75,13 +75,50 @@ public class CertificateTransparencyService extends ICertificateTransparencyMana
public void onBootPhase(int phase) {
switch (phase) {
case SystemService.PHASE_BOOT_COMPLETED:
if (certificateTransparencyJob()) {
mCertificateTransparencyJob.initialize();
} else {
mFlagsListener.initialize();
}
DeviceConfig.addOnPropertiesChangedListener(
Config.NAMESPACE_NETWORK_SECURITY,
Executors.newSingleThreadExecutor(),
this);
onPropertiesChanged(
new Properties.Builder(Config.NAMESPACE_NETWORK_SECURITY).build());
break;
default:
}
}
@Override
public void onPropertiesChanged(Properties properties) {
if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
return;
}
if (DeviceConfig.getBoolean(
Config.NAMESPACE_NETWORK_SECURITY,
Config.FLAG_SERVICE_ENABLED,
/* defaultValue= */ true)) {
startService();
} else {
stopService();
}
}
private void startService() {
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyService start");
}
if (!started) {
mCertificateTransparencyJob.schedule();
started = true;
}
}
private void stopService() {
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyService stop");
}
if (started) {
mCertificateTransparencyJob.cancel();
started = false;
}
}
}