Add CT log list data verification after download
The verification step assumes the metadata file, downloaded together with
the log list data, contains a signature generated using the public key
available at the certificate-transparency-community-site from Google
https://github.com/google/certificate-transparency-community-site/blob/master/docs/google/known-logs.md
Flag: com.android.net.ct.flags.certificate_transparency_service
Bug: 319829948
Test: atest NetworkSecurityUnitTests
Change-Id: If5fefb33b00c49a8f043d9b030f344f7917affbc
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 f35b163..b2ef345 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -15,39 +15,68 @@
*/
package com.android.server.net.ct;
+import android.annotation.RequiresApi;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
+import android.os.Build;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.io.IOException;
import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.Signature;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
/** Helper class to download certificate transparency log files. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
class CertificateTransparencyDownloader extends BroadcastReceiver {
private static final String TAG = "CertificateTransparencyDownloader";
+ // TODO: move key to a DeviceConfig flag.
+ private static final byte[] PUBLIC_KEY_BYTES =
+ Base64.getDecoder()
+ .decode(
+ "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsu0BHGnQ++W2CTdyZyxv"
+ + "HHRALOZPlnu/VMVgo2m+JZ8MNbAOH2cgXb8mvOj8flsX/qPMuKIaauO+PwROMjiq"
+ + "fUpcFm80Kl7i97ZQyBDYKm3MkEYYpGN+skAR2OebX9G2DfDqFY8+jUpOOWtBNr3L"
+ + "rmVcwx+FcFdMjGDlrZ5JRmoJ/SeGKiORkbbu9eY1Wd0uVhz/xI5bQb0OgII7hEj+"
+ + "i/IPbJqOHgB8xQ5zWAJJ0DmG+FM6o7gk403v6W3S8qRYiR84c50KppGwe4YqSMkF"
+ + "bLDleGQWLoaDSpEWtESisb4JiLaY4H+Kk0EyAhPSb+49JfUozYl+lf7iFN3qRq/S"
+ + "IXXTh6z0S7Qa8EYDhKGCrpI03/+qprwy+my6fpWHi6aUIk4holUCmWvFxZDfixox"
+ + "K0RlqbFDl2JXMBquwlQpm8u5wrsic1ksIv9z8x9zh4PJqNpCah0ciemI3YGRQqSe"
+ + "/mRRXBiSn9YQBUPcaeqCYan+snGADFwHuXCd9xIAdFBolw9R9HTedHGUfVXPJDiF"
+ + "4VusfX6BRR/qaadB+bqEArF/TzuDUr6FvOR4o8lUUxgLuZ/7HO+bHnaPFKYHHSm+"
+ + "+z1lVDhhYuSZ8ax3T0C3FZpb7HMjZtpEorSV5ElKJEJwrhrBCMOD8L01EoSPrGlS"
+ + "1w22i9uGHMn/uGQKo28u7AsCAwEAAQ==");
+
private final Context mContext;
private final DataStore mDataStore;
private final DownloadHelper mDownloadHelper;
private final CertificateTransparencyInstaller mInstaller;
+ private final byte[] mPublicKey;
@VisibleForTesting
CertificateTransparencyDownloader(
Context context,
DataStore dataStore,
DownloadHelper downloadHelper,
- CertificateTransparencyInstaller installer) {
+ CertificateTransparencyInstaller installer,
+ byte[] publicKey) {
mContext = context;
mDataStore = dataStore;
mDownloadHelper = downloadHelper;
mInstaller = installer;
+ mPublicKey = publicKey;
}
CertificateTransparencyDownloader(Context context, DataStore dataStore) {
@@ -55,13 +84,14 @@
context,
dataStore,
new DownloadHelper(context),
- new CertificateTransparencyInstaller());
+ new CertificateTransparencyInstaller(),
+ PUBLIC_KEY_BYTES);
}
void registerReceiver() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
- mContext.registerReceiver(this, intentFilter);
+ mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED);
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyDownloader initialized successfully");
@@ -139,12 +169,22 @@
return;
}
- // TODO: 1. verify file signature, 2. validate file content.
+ boolean success = false;
+ try {
+ success = verify(contentUri, metadataUri);
+ } 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");
+ return;
+ }
+
+ // TODO: validate file content.
String version = mDataStore.getProperty(Config.VERSION_PENDING);
String contentUrl = mDataStore.getProperty(Config.CONTENT_URL_PENDING);
String metadataUrl = mDataStore.getProperty(Config.METADATA_URL_PENDING);
- boolean success = false;
try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
success = mInstaller.install(inputStream, version);
} catch (IOException e) {
@@ -161,6 +201,19 @@
}
}
+ private boolean verify(Uri file, Uri signature) throws IOException, GeneralSecurityException {
+ Signature verifier = Signature.getInstance("SHA256withRSA");
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ verifier.initVerify(keyFactory.generatePublic(new X509EncodedKeySpec(mPublicKey)));
+ ContentResolver contentResolver = mContext.getContentResolver();
+
+ try (InputStream fileStream = contentResolver.openInputStream(file);
+ InputStream signatureStream = contentResolver.openInputStream(signature)) {
+ verifier.update(fileStream.readAllBytes());
+ return verifier.verify(signatureStream.readAllBytes());
+ }
+ }
+
private long download(String url) {
try {
return mDownloadHelper.startDownload(url);
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
index fdac434..f196abb 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -17,17 +17,18 @@
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+import android.annotation.RequiresApi;
import android.content.Context;
+import android.os.Build;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.text.TextUtils;
import android.util.Log;
-import com.android.modules.utils.build.SdkLevel;
-
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";
@@ -54,7 +55,7 @@
@Override
public void onPropertiesChanged(Properties properties) {
- if (!SdkLevel.isAtLeastV() || !NAMESPACE_TETHERING.equals(properties.getNamespace())) {
+ if (!NAMESPACE_TETHERING.equals(properties.getNamespace())) {
return;
}
@@ -85,6 +86,8 @@
return;
}
+ // TODO: handle the case where there is already a pending download.
+
mDataStore.setProperty(Config.VERSION_PENDING, newVersion);
mDataStore.setProperty(Config.CONTENT_URL_PENDING, newContentUrl);
mDataStore.setProperty(Config.METADATA_URL_PENDING, newMetadataUrl);
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 5131a71..a056c35 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
@@ -40,7 +40,17 @@
import org.mockito.MockitoAnnotations;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.Signature;
/** Tests for the {@link CertificateTransparencyDownloader}. */
@RunWith(JUnit4.class)
@@ -49,15 +59,20 @@
@Mock private DownloadHelper mDownloadHelper;
@Mock private CertificateTransparencyInstaller mCertificateTransparencyInstaller;
+ private PrivateKey mPrivateKey;
private Context mContext;
private File mTempFile;
private DataStore mDataStore;
private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
@Before
- public void setUp() throws IOException {
+ public void setUp() throws IOException, NoSuchAlgorithmException {
MockitoAnnotations.initMocks(this);
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+ KeyPair keyPair = instance.generateKeyPair();
+ mPrivateKey = keyPair.getPrivate();
+
mContext = InstrumentationRegistry.getInstrumentation().getContext();
mTempFile = File.createTempFile("datastore-test", ".properties");
mDataStore = new DataStore(mTempFile);
@@ -65,7 +80,11 @@
mCertificateTransparencyDownloader =
new CertificateTransparencyDownloader(
- mContext, mDataStore, mDownloadHelper, mCertificateTransparencyInstaller);
+ mContext,
+ mDataStore,
+ mDownloadHelper,
+ mCertificateTransparencyInstaller,
+ keyPair.getPublic().getEncoded());
}
@After
@@ -128,23 +147,16 @@
}
@Test
- public void testDownloader_handleContentCompleteInstallSuccessful() throws IOException {
+ public void testDownloader_handleContentCompleteInstallSuccessful() throws Exception {
String version = "666";
- mDataStore.setProperty(Config.VERSION_PENDING, version);
-
- long metadataId = 123;
- mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
- Uri metadataUri = Uri.fromFile(File.createTempFile("log_list-metadata", "txt"));
- mDataStore.setProperty(Config.METADATA_URL_PENDING, metadataUri.toString());
- when(mDownloadHelper.getUri(metadataId)).thenReturn(metadataUri);
-
long contentId = 666;
- mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
- when(mDownloadHelper.isSuccessful(contentId)).thenReturn(true);
- Uri contentUri = Uri.fromFile(File.createTempFile("log_list", "json"));
- mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUri.toString());
- when(mDownloadHelper.getUri(contentId)).thenReturn(contentUri);
+ File logListFile = File.createTempFile("log_list", "json");
+ Uri contentUri = Uri.fromFile(logListFile);
+ long metadataId = 123;
+ File metadataFile = sign(logListFile);
+ Uri metadataUri = Uri.fromFile(metadataFile);
+ setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(true);
assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
@@ -161,23 +173,16 @@
}
@Test
- public void testDownloader_handleContentCompleteInstallFails() throws IOException {
+ public void testDownloader_handleContentCompleteInstallFails() throws Exception {
String version = "666";
- mDataStore.setProperty(Config.VERSION_PENDING, version);
-
- long metadataId = 123;
- mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
- Uri metadataUri = Uri.fromFile(File.createTempFile("log_list-metadata", "txt"));
- mDataStore.setProperty(Config.METADATA_URL_PENDING, metadataUri.toString());
- when(mDownloadHelper.getUri(metadataId)).thenReturn(metadataUri);
-
long contentId = 666;
- mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
- when(mDownloadHelper.isSuccessful(contentId)).thenReturn(true);
- Uri contentUri = Uri.fromFile(File.createTempFile("log_list", "json"));
- mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUri.toString());
- when(mDownloadHelper.getUri(contentId)).thenReturn(contentUri);
+ File logListFile = File.createTempFile("log_list", "json");
+ Uri contentUri = Uri.fromFile(logListFile);
+ long metadataId = 123;
+ File metadataFile = sign(logListFile);
+ Uri metadataUri = Uri.fromFile(metadataFile);
+ setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(false);
mCertificateTransparencyDownloader.onReceive(
@@ -188,8 +193,56 @@
assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
}
+ @Test
+ public void testDownloader_handleContentCompleteVerificationFails() throws IOException {
+ String version = "666";
+ long contentId = 666;
+ Uri contentUri = Uri.fromFile(File.createTempFile("log_list", "json"));
+ long metadataId = 123;
+ Uri metadataUri = Uri.fromFile(File.createTempFile("log_list-wrong_metadata", "sig"));
+
+ setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ verify(mCertificateTransparencyInstaller, never()).install(any(), eq(version));
+ assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+ assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
+ assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
+ }
+
private Intent makeDownloadCompleteIntent(long downloadId) {
return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
}
+
+ private void setUpDownloadComplete(
+ String version, long metadataId, Uri metadataUri, long contentId, Uri contentUri)
+ throws IOException {
+ mDataStore.setProperty(Config.VERSION_PENDING, version);
+
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+ mDataStore.setProperty(Config.METADATA_URL_PENDING, metadataUri.toString());
+ when(mDownloadHelper.getUri(metadataId)).thenReturn(metadataUri);
+
+ mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
+ mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUri.toString());
+ when(mDownloadHelper.isSuccessful(contentId)).thenReturn(true);
+ when(mDownloadHelper.getUri(contentId)).thenReturn(contentUri);
+ }
+
+ private File sign(File file) throws IOException, GeneralSecurityException {
+ File signatureFile = File.createTempFile("log_list-metadata", "sig");
+ Signature signer = Signature.getInstance("SHA256withRSA");
+ signer.initSign(mPrivateKey);
+
+ try (InputStream fileStream = new FileInputStream(file);
+ OutputStream outputStream = new FileOutputStream(signatureFile)) {
+ signer.update(fileStream.readAllBytes());
+ outputStream.write(signer.sign());
+ }
+
+ return signatureFile;
+ }
}