Move public key to a DeviceConfig flag
Instead of hard-coding the public key value in code. Flag: com.android.net.ct.flags.certificate_transparency_service Bug: 319829948 Test: atest NetworkSecurityUnitTests Change-Id: Ided9a9402241d91cae3cee25f5511c3a881e087d
This commit is contained in:
parent
2f6d6421ad
commit
802c8f1a4d
4 changed files with 84 additions and 36 deletions
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.android.server.net.ct;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.RequiresApi;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
|
|
@ -31,10 +32,13 @@ 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;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Helper class to download certificate transparency log files. */
|
||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||
|
|
@ -42,41 +46,23 @@ 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;
|
||||
|
||||
@NonNull private Optional<PublicKey> mPublicKey = Optional.empty();
|
||||
|
||||
@VisibleForTesting
|
||||
CertificateTransparencyDownloader(
|
||||
Context context,
|
||||
DataStore dataStore,
|
||||
DownloadHelper downloadHelper,
|
||||
CertificateTransparencyInstaller installer,
|
||||
byte[] publicKey) {
|
||||
CertificateTransparencyInstaller installer) {
|
||||
mContext = context;
|
||||
mDataStore = dataStore;
|
||||
mDownloadHelper = downloadHelper;
|
||||
mInstaller = installer;
|
||||
mPublicKey = publicKey;
|
||||
}
|
||||
|
||||
CertificateTransparencyDownloader(Context context, DataStore dataStore) {
|
||||
|
|
@ -84,8 +70,7 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
|
|||
context,
|
||||
dataStore,
|
||||
new DownloadHelper(context),
|
||||
new CertificateTransparencyInstaller(),
|
||||
PUBLIC_KEY_BYTES);
|
||||
new CertificateTransparencyInstaller());
|
||||
}
|
||||
|
||||
void registerReceiver() {
|
||||
|
|
@ -98,6 +83,20 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
|
|||
}
|
||||
}
|
||||
|
||||
void setPublicKey(String publicKey) throws GeneralSecurityException {
|
||||
mPublicKey =
|
||||
Optional.of(
|
||||
KeyFactory.getInstance("RSA")
|
||||
.generatePublic(
|
||||
new X509EncodedKeySpec(
|
||||
Base64.getDecoder().decode(publicKey))));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void resetPublicKey() {
|
||||
mPublicKey = Optional.empty();
|
||||
}
|
||||
|
||||
void startMetadataDownload(String metadataUrl) {
|
||||
long downloadId = download(metadataUrl);
|
||||
if (downloadId == -1) {
|
||||
|
|
@ -202,9 +201,11 @@ class CertificateTransparencyDownloader extends BroadcastReceiver {
|
|||
}
|
||||
|
||||
private boolean verify(Uri file, Uri signature) throws IOException, GeneralSecurityException {
|
||||
if (!mPublicKey.isPresent()) {
|
||||
throw new InvalidKeyException("Missing public key for signature verification");
|
||||
}
|
||||
Signature verifier = Signature.getInstance("SHA256withRSA");
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
verifier.initVerify(keyFactory.generatePublic(new X509EncodedKeySpec(mPublicKey)));
|
||||
verifier.initVerify(mPublicKey.get());
|
||||
ContentResolver contentResolver = mContext.getContentResolver();
|
||||
|
||||
try (InputStream fileStream = contentResolver.openInputStream(file);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ 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. */
|
||||
|
|
@ -57,21 +58,35 @@ class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesC
|
|||
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, "");
|
||||
DeviceConfig.getString(
|
||||
Config.NAMESPACE_NETWORK_SECURITY,
|
||||
Config.FLAG_VERSION,
|
||||
/* defaultValue= */ "");
|
||||
String newContentUrl =
|
||||
DeviceConfig.getString(
|
||||
Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_CONTENT_URL, "");
|
||||
Config.NAMESPACE_NETWORK_SECURITY,
|
||||
Config.FLAG_CONTENT_URL,
|
||||
/* defaultValue= */ "");
|
||||
String newMetadataUrl =
|
||||
DeviceConfig.getString(
|
||||
Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_METADATA_URL, "");
|
||||
if (TextUtils.isEmpty(newVersion)
|
||||
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);
|
||||
|
|
@ -88,6 +103,13 @@ class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesC
|
|||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mCertificateTransparencyDownloader.setPublicKey(newPublicKey);
|
||||
} catch (GeneralSecurityException 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.VERSION_PENDING, newVersion);
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ final class Config {
|
|||
static final String FLAG_CONTENT_URL = FLAGS_PREFIX + "content_url";
|
||||
static final String FLAG_METADATA_URL = FLAGS_PREFIX + "metadata_url";
|
||||
static final String FLAG_VERSION = FLAGS_PREFIX + "version";
|
||||
static final String FLAG_PUBLIC_KEY = FLAGS_PREFIX + "public_key";
|
||||
|
||||
// properties
|
||||
static final String VERSION_PENDING = "version_pending";
|
||||
|
|
|
|||
|
|
@ -48,9 +48,10 @@ 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.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.util.Base64;
|
||||
|
||||
/** Tests for the {@link CertificateTransparencyDownloader}. */
|
||||
@RunWith(JUnit4.class)
|
||||
|
|
@ -60,18 +61,20 @@ public class CertificateTransparencyDownloaderTest {
|
|||
@Mock private CertificateTransparencyInstaller mCertificateTransparencyInstaller;
|
||||
|
||||
private PrivateKey mPrivateKey;
|
||||
private PublicKey mPublicKey;
|
||||
private Context mContext;
|
||||
private File mTempFile;
|
||||
private DataStore mDataStore;
|
||||
private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException, NoSuchAlgorithmException {
|
||||
public void setUp() throws IOException, GeneralSecurityException {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
|
||||
KeyPair keyPair = instance.generateKeyPair();
|
||||
mPrivateKey = keyPair.getPrivate();
|
||||
mPublicKey = keyPair.getPublic();
|
||||
|
||||
mContext = InstrumentationRegistry.getInstrumentation().getContext();
|
||||
mTempFile = File.createTempFile("datastore-test", ".properties");
|
||||
|
|
@ -80,16 +83,13 @@ public class CertificateTransparencyDownloaderTest {
|
|||
|
||||
mCertificateTransparencyDownloader =
|
||||
new CertificateTransparencyDownloader(
|
||||
mContext,
|
||||
mDataStore,
|
||||
mDownloadHelper,
|
||||
mCertificateTransparencyInstaller,
|
||||
keyPair.getPublic().getEncoded());
|
||||
mContext, mDataStore, mDownloadHelper, mCertificateTransparencyInstaller);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
mTempFile.delete();
|
||||
mCertificateTransparencyDownloader.resetPublicKey();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -155,6 +155,8 @@ public class CertificateTransparencyDownloaderTest {
|
|||
long metadataId = 123;
|
||||
File metadataFile = sign(logListFile);
|
||||
Uri metadataUri = Uri.fromFile(metadataFile);
|
||||
mCertificateTransparencyDownloader.setPublicKey(
|
||||
Base64.getEncoder().encodeToString(mPublicKey.getEncoded()));
|
||||
|
||||
setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
|
||||
when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(true);
|
||||
|
|
@ -212,6 +214,28 @@ public class CertificateTransparencyDownloaderTest {
|
|||
assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloader_handleContentCompleteMissingVerificationPublicKey()
|
||||
throws Exception {
|
||||
String version = "666";
|
||||
long contentId = 666;
|
||||
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);
|
||||
|
||||
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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue