remove support for unsigned repos
This has been discussed quite a bit now. It is very easy to generate a signed repo on the server, and supporting unsigned repos adds complexity and security issues, including "BZ-01-002 TOFU Requests too easy to recognize and intercept" from the audit. https://gitlab.com/fdroid/fdroidserver/merge_requests/48 closes #12 https://gitlab.com/fdroid/fdroidclient/issues/12
This commit is contained in:
parent
1c5256a5d7
commit
157b1e242f
@ -376,7 +376,7 @@ public class UpdateService extends IntentService implements ProgressListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address));
|
sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address));
|
||||||
RepoUpdater updater = RepoUpdater.createUpdaterFor(getBaseContext(), repo);
|
RepoUpdater updater = new RepoUpdater(getBaseContext(), repo);
|
||||||
updater.setProgressListener(this);
|
updater.setProgressListener(this);
|
||||||
try {
|
try {
|
||||||
updater.update();
|
updater.update();
|
||||||
|
@ -2,9 +2,12 @@ package org.fdroid.fdroid.updater;
|
|||||||
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.FDroidApp;
|
||||||
|
import org.fdroid.fdroid.Hasher;
|
||||||
import org.fdroid.fdroid.ProgressListener;
|
import org.fdroid.fdroid.ProgressListener;
|
||||||
import org.fdroid.fdroid.RepoXMLHandler;
|
import org.fdroid.fdroid.RepoXMLHandler;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
@ -20,31 +23,29 @@ import org.xml.sax.XMLReader;
|
|||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.FileReader;
|
import java.io.FileReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
import javax.xml.parsers.SAXParser;
|
import javax.xml.parsers.SAXParser;
|
||||||
import javax.xml.parsers.SAXParserFactory;
|
import javax.xml.parsers.SAXParserFactory;
|
||||||
|
|
||||||
abstract public class RepoUpdater {
|
public class RepoUpdater {
|
||||||
|
|
||||||
private static final String TAG = "fdroid.RepoUpdater";
|
private static final String TAG = "fdroid.RepoUpdater";
|
||||||
|
|
||||||
public static final String PROGRESS_TYPE_PROCESS_XML = "processingXml";
|
public static final String PROGRESS_TYPE_PROCESS_XML = "processingXml";
|
||||||
|
|
||||||
public static final String PROGRESS_DATA_REPO_ADDRESS = "repoAddress";
|
public static final String PROGRESS_DATA_REPO_ADDRESS = "repoAddress";
|
||||||
|
|
||||||
public static RepoUpdater createUpdaterFor(Context ctx, Repo repo) {
|
|
||||||
if (repo.fingerprint == null && repo.pubkey == null) {
|
|
||||||
return new UnsignedRepoUpdater(ctx, repo);
|
|
||||||
}
|
|
||||||
return new SignedRepoUpdater(ctx, repo);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected final Context context;
|
protected final Context context;
|
||||||
protected final Repo repo;
|
protected final Repo repo;
|
||||||
private List<App> apps = new ArrayList<>();
|
private List<App> apps = new ArrayList<>();
|
||||||
@ -70,15 +71,47 @@ abstract public class RepoUpdater {
|
|||||||
public List<Apk> getApks() { return apks; }
|
public List<Apk> getApks() { return apks; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For example, you may want to unzip a jar file to get the index inside,
|
* All repos are represented by a signed jar file, {@code index.jar}, which contains
|
||||||
* or if the file is not compressed, you can just return a reference to
|
* a single file, {@code index.xml}. This takes the {@code index.jar}, verifies the
|
||||||
* the downloaded file.
|
* signature, then returns the unzipped {@code index.xml}.
|
||||||
*
|
*
|
||||||
* @throws UpdateException All error states will come from here.
|
* @throws UpdateException All error states will come from here.
|
||||||
*/
|
*/
|
||||||
protected abstract File getIndexFromFile(File downloadedFile) throws UpdateException;
|
protected File getIndexFromFile(File downloadedFile) throws UpdateException {
|
||||||
|
final Date updateTime = new Date(System.currentTimeMillis());
|
||||||
|
Log.d(TAG, "Getting signed index from " + repo.address + " at " +
|
||||||
|
Utils.formatLogDate(updateTime));
|
||||||
|
|
||||||
protected abstract String getIndexAddress();
|
final File indexJar = downloadedFile;
|
||||||
|
File indexXml = null;
|
||||||
|
|
||||||
|
// Don't worry about checking the status code for 200. If it was a
|
||||||
|
// successful download, then we will have a file ready to use:
|
||||||
|
if (indexJar != null && indexJar.exists()) {
|
||||||
|
|
||||||
|
// Due to a bug in android 5.0 lollipop, the inclusion of BouncyCastle causes
|
||||||
|
// breakage when verifying the signature of the downloaded .jar. For more
|
||||||
|
// details, check out https://gitlab.com/fdroid/fdroidclient/issues/111.
|
||||||
|
try {
|
||||||
|
FDroidApp.disableSpongyCastleOnLollipop();
|
||||||
|
indexXml = extractIndexFromJar(indexJar);
|
||||||
|
} finally {
|
||||||
|
FDroidApp.enableSpongyCastleOnLollipop();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return indexXml;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getIndexAddress() {
|
||||||
|
try {
|
||||||
|
String versionName = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
|
||||||
|
return repo.address + "/index.jar?client_version=" + versionName;
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return repo.address + "/index.jar";
|
||||||
|
}
|
||||||
|
|
||||||
protected Downloader downloadIndex() throws UpdateException {
|
protected Downloader downloadIndex() throws UpdateException {
|
||||||
Downloader downloader = null;
|
Downloader downloader = null;
|
||||||
@ -128,7 +161,6 @@ abstract public class RepoUpdater {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void update() throws UpdateException {
|
public void update() throws UpdateException {
|
||||||
|
|
||||||
File downloadedFile = null;
|
File downloadedFile = null;
|
||||||
@ -261,4 +293,84 @@ abstract public class RepoUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean verifyCerts(JarEntry item) throws UpdateException {
|
||||||
|
final Certificate[] certs = item.getCertificates();
|
||||||
|
if (certs == null || certs.length == 0) {
|
||||||
|
throw new UpdateException(repo, "No signature found in index");
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Index has " + certs.length + " signature(s)");
|
||||||
|
boolean match = false;
|
||||||
|
for (final Certificate cert : certs) {
|
||||||
|
String certdata = Hasher.hex(cert);
|
||||||
|
if (repo.pubkey == null && repo.fingerprint != null) {
|
||||||
|
String certFingerprint = Utils.calcFingerprint(cert);
|
||||||
|
Log.d(TAG, "No public key for repo " + repo.address + " yet, but it does have a fingerprint, so comparing them.");
|
||||||
|
Log.d(TAG, "Repo fingerprint: " + repo.fingerprint);
|
||||||
|
Log.d(TAG, "Cert fingerprint: " + certFingerprint);
|
||||||
|
if (repo.fingerprint.equalsIgnoreCase(certFingerprint)) {
|
||||||
|
repo.pubkey = certdata;
|
||||||
|
usePubkeyInJar = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (repo.pubkey != null && repo.pubkey.equals(certdata)) {
|
||||||
|
Log.d(TAG, "Checking repo public key against cert found in jar.");
|
||||||
|
match = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected File extractIndexFromJar(File indexJar) throws UpdateException {
|
||||||
|
File indexFile = null;
|
||||||
|
JarFile jarFile = null;
|
||||||
|
try {
|
||||||
|
jarFile = new JarFile(indexJar, true);
|
||||||
|
JarEntry indexEntry = (JarEntry) jarFile.getEntry("index.xml");
|
||||||
|
|
||||||
|
indexFile = File.createTempFile("index-", "-extracted.xml", context.getCacheDir());
|
||||||
|
InputStream input = null;
|
||||||
|
OutputStream output = null;
|
||||||
|
try {
|
||||||
|
/*
|
||||||
|
* JarFile.getInputStream() provides the signature check, even
|
||||||
|
* though the Android docs do not mention this, the Java docs do
|
||||||
|
* and Android seems to implement it the same:
|
||||||
|
* http://docs.oracle.com/javase/6/docs/api/java/util/jar/JarFile.html#getInputStream(java.util.zip.ZipEntry)
|
||||||
|
* https://developer.android.com/reference/java/util/jar/JarFile.html#getInputStream(java.util.zip.ZipEntry)
|
||||||
|
*/
|
||||||
|
input = jarFile.getInputStream(indexEntry);
|
||||||
|
output = new FileOutputStream(indexFile);
|
||||||
|
Utils.copy(input, output);
|
||||||
|
} finally {
|
||||||
|
Utils.closeQuietly(output);
|
||||||
|
Utils.closeQuietly(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can only read certificates from jar after it has been read
|
||||||
|
// completely, so we put it after the copy above...
|
||||||
|
if (!verifyCerts(indexEntry)) {
|
||||||
|
indexFile.delete();
|
||||||
|
throw new UpdateException(repo, "Index signature mismatch");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (indexFile != null) {
|
||||||
|
indexFile.delete();
|
||||||
|
}
|
||||||
|
throw new UpdateException(
|
||||||
|
repo, "Error opening signed index", e);
|
||||||
|
} finally {
|
||||||
|
if (jarFile != null) {
|
||||||
|
try {
|
||||||
|
jarFile.close();
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexFile;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,153 +0,0 @@
|
|||||||
package org.fdroid.fdroid.updater;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.fdroid.fdroid.FDroidApp;
|
|
||||||
import org.fdroid.fdroid.Hasher;
|
|
||||||
import org.fdroid.fdroid.R;
|
|
||||||
import org.fdroid.fdroid.Utils;
|
|
||||||
import org.fdroid.fdroid.data.Repo;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.security.cert.Certificate;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.jar.JarEntry;
|
|
||||||
import java.util.jar.JarFile;
|
|
||||||
|
|
||||||
public class SignedRepoUpdater extends RepoUpdater {
|
|
||||||
|
|
||||||
private static final String TAG = "fdroid.SignedRepoUpdater";
|
|
||||||
|
|
||||||
public SignedRepoUpdater(Context ctx, Repo repo) {
|
|
||||||
super(ctx, repo);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean verifyCerts(JarEntry item) throws UpdateException {
|
|
||||||
final Certificate[] certs = item.getCertificates();
|
|
||||||
if (certs == null || certs.length == 0) {
|
|
||||||
throw new UpdateException(repo, "No signature found in index");
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "Index has " + certs.length + " signature(s)");
|
|
||||||
boolean match = false;
|
|
||||||
for (final Certificate cert : certs) {
|
|
||||||
String certdata = Hasher.hex(cert);
|
|
||||||
if (repo.pubkey == null && repo.fingerprint != null) {
|
|
||||||
String certFingerprint = Utils.calcFingerprint(cert);
|
|
||||||
Log.d(TAG, "No public key for repo " + repo.address + " yet, but it does have a fingerprint, so comparing them.");
|
|
||||||
Log.d(TAG, "Repo fingerprint: " + repo.fingerprint);
|
|
||||||
Log.d(TAG, "Cert fingerprint: " + certFingerprint);
|
|
||||||
if (repo.fingerprint.equalsIgnoreCase(certFingerprint)) {
|
|
||||||
repo.pubkey = certdata;
|
|
||||||
usePubkeyInJar = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (repo.pubkey != null && repo.pubkey.equals(certdata)) {
|
|
||||||
Log.d(TAG, "Checking repo public key against cert found in jar.");
|
|
||||||
match = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected File extractIndexFromJar(File indexJar) throws UpdateException {
|
|
||||||
File indexFile = null;
|
|
||||||
JarFile jarFile = null;
|
|
||||||
try {
|
|
||||||
jarFile = new JarFile(indexJar, true);
|
|
||||||
JarEntry indexEntry = (JarEntry)jarFile.getEntry("index.xml");
|
|
||||||
|
|
||||||
indexFile = File.createTempFile("index-", "-extracted.xml", context.getCacheDir());
|
|
||||||
InputStream input = null;
|
|
||||||
OutputStream output = null;
|
|
||||||
try {
|
|
||||||
/*
|
|
||||||
* JarFile.getInputStream() provides the signature check, even
|
|
||||||
* though the Android docs do not mention this, the Java docs do
|
|
||||||
* and Android seems to implement it the same:
|
|
||||||
* http://docs.oracle.com/javase/6/docs/api/java/util/jar/JarFile.html#getInputStream(java.util.zip.ZipEntry)
|
|
||||||
* https://developer.android.com/reference/java/util/jar/JarFile.html#getInputStream(java.util.zip.ZipEntry)
|
|
||||||
*/
|
|
||||||
input = jarFile.getInputStream(indexEntry);
|
|
||||||
output = new FileOutputStream(indexFile);
|
|
||||||
Utils.copy(input, output);
|
|
||||||
} finally {
|
|
||||||
Utils.closeQuietly(output);
|
|
||||||
Utils.closeQuietly(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Can only read certificates from jar after it has been read
|
|
||||||
// completely, so we put it after the copy above...
|
|
||||||
if (!verifyCerts(indexEntry)) {
|
|
||||||
indexFile.delete();
|
|
||||||
throw new UpdateException(repo, "Index signature mismatch");
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (indexFile != null) {
|
|
||||||
indexFile.delete();
|
|
||||||
}
|
|
||||||
throw new UpdateException(
|
|
||||||
repo, "Error opening signed index", e);
|
|
||||||
} finally {
|
|
||||||
if (jarFile != null) {
|
|
||||||
try {
|
|
||||||
jarFile.close();
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return indexFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getIndexAddress() {
|
|
||||||
try {
|
|
||||||
String versionName = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
|
|
||||||
return repo.address + "/index.jar?client_version=" + versionName;
|
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return repo.address + "/index.jar";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* As this is a signed repo - we download the jar file,
|
|
||||||
* check the signature, and extract the index file
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected File getIndexFromFile(File downloadedFile) throws
|
|
||||||
UpdateException {
|
|
||||||
final Date updateTime = new Date(System.currentTimeMillis());
|
|
||||||
Log.d(TAG, "Getting signed index from " + repo.address + " at " +
|
|
||||||
Utils.formatLogDate(updateTime));
|
|
||||||
|
|
||||||
final File indexJar = downloadedFile;
|
|
||||||
File indexXml = null;
|
|
||||||
|
|
||||||
// Don't worry about checking the status code for 200. If it was a
|
|
||||||
// successful download, then we will have a file ready to use:
|
|
||||||
if (indexJar != null && indexJar.exists()) {
|
|
||||||
|
|
||||||
// Due to a bug in android 5.0 lollipop, the inclusion of BouncyCastle causes
|
|
||||||
// breakage when verifying the signature of the downloaded .jar. For more
|
|
||||||
// details, check out https://gitlab.com/fdroid/fdroidclient/issues/111.
|
|
||||||
try {
|
|
||||||
FDroidApp.disableSpongyCastleOnLollipop();
|
|
||||||
indexXml = extractIndexFromJar(indexJar);
|
|
||||||
} finally {
|
|
||||||
FDroidApp.enableSpongyCastleOnLollipop();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return indexXml;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
package org.fdroid.fdroid.updater;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.fdroid.fdroid.data.Repo;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
public class UnsignedRepoUpdater extends RepoUpdater {
|
|
||||||
|
|
||||||
private static final String TAG = "fdroid.UnsignedRepoUpdater";
|
|
||||||
|
|
||||||
public UnsignedRepoUpdater(Context ctx, Repo repo) {
|
|
||||||
super(ctx, repo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected File getIndexFromFile(File file) throws UpdateException {
|
|
||||||
Log.d(TAG, "Getting unsigned index from " + getIndexAddress());
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getIndexAddress() {
|
|
||||||
return repo.address + "/index.xml";
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,8 +14,8 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@TargetApi(8)
|
@TargetApi(8)
|
||||||
public class SignedRepoUpdaterTest extends InstrumentationTestCase {
|
public class RepoUpdaterTest extends InstrumentationTestCase {
|
||||||
private static final String TAG = "SignedRepoUpdaterTest";
|
private static final String TAG = "RepoUpdaterTest";
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
private RepoUpdater repoUpdater;
|
private RepoUpdater repoUpdater;
|
||||||
@ -29,7 +29,7 @@ public class SignedRepoUpdaterTest extends InstrumentationTestCase {
|
|||||||
testFilesDir = TestUtils.getWriteableDir(getInstrumentation());
|
testFilesDir = TestUtils.getWriteableDir(getInstrumentation());
|
||||||
Repo repo = new Repo();
|
Repo repo = new Repo();
|
||||||
repo.pubkey = this.simpleIndexPubkey;
|
repo.pubkey = this.simpleIndexPubkey;
|
||||||
repoUpdater = RepoUpdater.createUpdaterFor(context, repo);
|
repoUpdater = new RepoUpdater(context, repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testExtractIndexFromJar() {
|
public void testExtractIndexFromJar() {
|
Loading…
x
Reference in New Issue
Block a user