1
0
mirror of https://gitlab.com/fdroid/fdroidserver.git synced 2024-10-03 17:50:11 +02:00

Merge branch 'AllowedSigningKeys' into 'master'

add AllowedSigningKeys metadata for enforcing APK signatures

See merge request fdroid/fdroidserver!984
This commit is contained in:
Chirayu Desai 2021-08-05 15:35:09 +00:00
commit bbda73f6c7
8 changed files with 192 additions and 35 deletions

View File

@ -47,6 +47,7 @@ metadata_v0:
- cd fdroiddata - cd fdroiddata
- ../tests/dump_internal_metadata_format.py - ../tests/dump_internal_metadata_format.py
- sed -i - sed -i
-e '/AllowedAPKSigningKeys:/d'
-e '/Liberapay:/d' -e '/Liberapay:/d'
-e '/OpenCollective/d' -e '/OpenCollective/d'
metadata/dump_*/*.yaml metadata/dump_*/*.yaml

View File

@ -90,6 +90,8 @@ yaml_app_field_order = [
'\n', '\n',
'Builds', 'Builds',
'\n', '\n',
'AllowedAPKSigningKeys',
'\n',
'MaintainerNotes', 'MaintainerNotes',
'\n', '\n',
'ArchivePolicy', 'ArchivePolicy',
@ -145,6 +147,7 @@ class App(dict):
self.RepoType = '' self.RepoType = ''
self.Repo = '' self.Repo = ''
self.Binaries = None self.Binaries = None
self.AllowedAPKSigningKeys = []
self.MaintainerNotes = '' self.MaintainerNotes = ''
self.ArchivePolicy = None self.ArchivePolicy = None
self.AutoUpdateMode = 'None' self.AutoUpdateMode = 'None'
@ -186,8 +189,6 @@ class App(dict):
return Build() return Build()
TYPE_UNKNOWN = 0
TYPE_OBSOLETE = 1
TYPE_STRING = 2 TYPE_STRING = 2
TYPE_BOOL = 3 TYPE_BOOL = 3
TYPE_LIST = 4 TYPE_LIST = 4
@ -201,9 +202,8 @@ fieldtypes = {
'MaintainerNotes': TYPE_MULTILINE, 'MaintainerNotes': TYPE_MULTILINE,
'Categories': TYPE_LIST, 'Categories': TYPE_LIST,
'AntiFeatures': TYPE_LIST, 'AntiFeatures': TYPE_LIST,
'AllowedAPKSigningKeys': TYPE_LIST,
'Build': TYPE_BUILD, 'Build': TYPE_BUILD,
'BuildVersion': TYPE_OBSOLETE,
'UseBuilt': TYPE_OBSOLETE,
} }
@ -437,6 +437,10 @@ valuetypes = {
r'^http[s]?://', r'^http[s]?://',
["Binaries"]), ["Binaries"]),
FieldValidator("AllowedAPKSigningKeys",
r'^[a-fA-F0-9]{64}$',
["AllowedAPKSigningKeys"]),
FieldValidator("Archive Policy", FieldValidator("Archive Policy",
r'^[0-9]+ versions$', r'^[0-9]+ versions$',
["ArchivePolicy"]), ["ArchivePolicy"]),
@ -931,6 +935,14 @@ def write_yaml(mf, app):
cm.update({field: _builds_to_yaml(app)}) cm.update({field: _builds_to_yaml(app)})
elif field == 'CurrentVersionCode': elif field == 'CurrentVersionCode':
cm.update({field: _field_to_yaml(TYPE_INT, getattr(app, field))}) cm.update({field: _field_to_yaml(TYPE_INT, getattr(app, field))})
elif field == 'AllowedAPKSigningKeys':
value = getattr(app, field)
if value:
value = [str(i).lower() for i in value]
if len(value) == 1:
cm.update({field: _field_to_yaml(TYPE_STRING, value[0])})
else:
cm.update({field: _field_to_yaml(TYPE_LIST, value)})
else: else:
cm.update({field: _field_to_yaml(fieldtype(field), getattr(app, field))}) cm.update({field: _field_to_yaml(fieldtype(field), getattr(app, field))})

View File

@ -36,6 +36,8 @@ import yaml
import copy import copy
from datetime import datetime from datetime import datetime
from argparse import ArgumentParser from argparse import ArgumentParser
from pathlib import Path
try: try:
from yaml import CSafeLoader as SafeLoader from yaml import CSafeLoader as SafeLoader
except ImportError: except ImportError:
@ -51,6 +53,7 @@ from . import metadata
from .exception import BuildException, FDroidException from .exception import BuildException, FDroidException
from PIL import Image, PngImagePlugin from PIL import Image, PngImagePlugin
if hasattr(Image, 'DecompressionBombWarning'): if hasattr(Image, 'DecompressionBombWarning'):
warnings.simplefilter('error', Image.DecompressionBombWarning) warnings.simplefilter('error', Image.DecompressionBombWarning)
Image.MAX_IMAGE_PIXELS = 0xffffff # 4096x4096 Image.MAX_IMAGE_PIXELS = 0xffffff # 4096x4096
@ -688,7 +691,6 @@ def insert_obbs(repodir, apps, apks):
list of current, valid apps list of current, valid apps
apks apks
current information on all APKs current information on all APKs
""" """
def obbWarnDelete(f, msg): def obbWarnDelete(f, msg):
logging.warning(msg + ' ' + f) logging.warning(msg + ' ' + f)
@ -2223,6 +2225,31 @@ def get_apps_with_packages(apps, apks):
return appsWithPackages return appsWithPackages
def get_apks_without_allowed_signatures(app, apk):
"""Check the APK or package has been signed by one of the allowed signing certificates.
The fingerprint of the signing certificate is the standard X.509
SHA-256 fingerprint as a hex string. It can be fetched from an
APK using:
apksigner verify --print-certs my.apk | grep SHA-256
Parameters
----------
app
The app which declares the AllowedSigningKey
apk
The APK to check
"""
if not app or not apk:
return
allowed_signer_keys = app.get('AllowedAPKSigningKeys', [])
if not allowed_signer_keys:
return
if apk['signer'] not in allowed_signer_keys:
return apk['apkName']
def prepare_apps(apps, apks, repodir): def prepare_apps(apps, apks, repodir):
"""Encapsulate all necessary preparation steps before we can build an index out of apps and apks. """Encapsulate all necessary preparation steps before we can build an index out of apps and apks.
@ -2372,6 +2399,23 @@ def main():
appid_has_apks = set() appid_has_apks = set()
appid_has_repo_files = set() appid_has_repo_files = set()
for apk in apks: for apk in apks:
to_remove = get_apks_without_allowed_signatures(apps.get(apk['packageName']), apk)
if to_remove:
apks.remove(apk)
logging.warning(
_('"{path}" is signed by a key that is not allowed:').format(
path=to_remove
)
+ '\n'
+ apk['signer']
)
if options.delete_unknown:
for d in repodirs:
path = Path(d) / to_remove
if path.exists():
logging.warning(_('Removing {path}"').format(path=path))
path.unlink()
if apk['apkName'].endswith('.apk'): if apk['apkName'].endswith('.apk'):
appid_has_apks.add(apk['packageName']) appid_has_apks.add(apk['packageName'])
else: else:

View File

@ -1,3 +1,4 @@
AllowedAPKSigningKeys: []
AntiFeatures: [] AntiFeatures: []
ArchivePolicy: 4 versions ArchivePolicy: 4 versions
AuthorEmail: null AuthorEmail: null

View File

@ -1,3 +1,4 @@
AllowedAPKSigningKeys: []
AntiFeatures: [] AntiFeatures: []
ArchivePolicy: null ArchivePolicy: null
AuthorEmail: null AuthorEmail: null

View File

@ -1,3 +1,4 @@
AllowedAPKSigningKeys: []
AntiFeatures: [] AntiFeatures: []
ArchivePolicy: null ArchivePolicy: null
AuthorEmail: null AuthorEmail: null

View File

@ -1,3 +1,4 @@
AllowedAPKSigningKeys: []
AntiFeatures: [] AntiFeatures: []
ArchivePolicy: 9 versions ArchivePolicy: 9 versions
AuthorEmail: null AuthorEmail: null

View File

@ -62,8 +62,11 @@ DONATION_FIELDS = ('Donate', 'Liberapay', 'OpenCollective')
class Options: class Options:
allow_disabled_algorithms = False allow_disabled_algorithms = False
clean = False clean = False
delete_unknown = False
nosign = False
pretty = True pretty = True
rename_apks = False rename_apks = False
verbose = False
class UpdateTest(unittest.TestCase): class UpdateTest(unittest.TestCase):
@ -82,11 +85,13 @@ class UpdateTest(unittest.TestCase):
os.makedirs(self.tmpdir) os.makedirs(self.tmpdir)
os.chdir(self.basedir) os.chdir(self.basedir)
fdroidserver.common.config = None
fdroidserver.common.options = None
def testInsertStoreMetadata(self): def testInsertStoreMetadata(self):
config = dict() config = dict()
fdroidserver.common.fill_config_defaults(config) fdroidserver.common.fill_config_defaults(config)
fdroidserver.update.config = config fdroidserver.update.config = config
fdroidserver.update.options = fdroidserver.common.options
os.chdir(os.path.join(localmodule, 'tests')) os.chdir(os.path.join(localmodule, 'tests'))
shutil.rmtree(os.path.join('repo', 'info.guardianproject.urzip'), ignore_errors=True) shutil.rmtree(os.path.join('repo', 'info.guardianproject.urzip'), ignore_errors=True)
@ -193,11 +198,10 @@ class UpdateTest(unittest.TestCase):
fdroidserver.common.config = config fdroidserver.common.config = config
fdroidserver.update.config = config fdroidserver.update.config = config
os.chdir(os.path.join(localmodule, 'tests')) os.chdir(os.path.join(localmodule, 'tests'))
fdroidserver.update.options = type('', (), {})() fdroidserver.common.options = Options
fdroidserver.update.options = fdroidserver.common.options
fdroidserver.update.options.clean = True fdroidserver.update.options.clean = True
fdroidserver.update.options.delete_unknown = True fdroidserver.update.options.delete_unknown = True
fdroidserver.update.options.rename_apks = False
fdroidserver.update.options.allow_disabled_algorithms = False
apps = fdroidserver.metadata.read_metadata() apps = fdroidserver.metadata.read_metadata()
knownapks = fdroidserver.common.KnownApks() knownapks = fdroidserver.common.KnownApks()
@ -247,6 +251,7 @@ class UpdateTest(unittest.TestCase):
{'packageName': 'apks', 'name': 'fromapk2', 'versionCode': 2}, {'packageName': 'apks', 'name': 'fromapk2', 'versionCode': 2},
{'packageName': 'apks', 'name': testvalue, 'versionCode': 3}, {'packageName': 'apks', 'name': testvalue, 'versionCode': 3},
] ]
fdroidserver.common.options = Options
fdroidserver.update.insert_missing_app_names_from_apks(apps, apks) fdroidserver.update.insert_missing_app_names_from_apks(apps, apks)
for appid, app in apps.items(): for appid, app in apps.items():
if appid == 'none': if appid == 'none':
@ -276,11 +281,10 @@ class UpdateTest(unittest.TestCase):
fdroidserver.common.fill_config_defaults(config) fdroidserver.common.fill_config_defaults(config)
fdroidserver.common.config = config fdroidserver.common.config = config
fdroidserver.update.config = config fdroidserver.update.config = config
fdroidserver.update.options = type('', (), {})() fdroidserver.common.options = Options
fdroidserver.update.options = fdroidserver.common.options
fdroidserver.update.options.clean = True fdroidserver.update.options.clean = True
fdroidserver.update.options.delete_unknown = True fdroidserver.update.options.delete_unknown = True
fdroidserver.update.options.rename_apks = False
fdroidserver.update.options.allow_disabled_algorithms = False
apps = fdroidserver.metadata.read_metadata() apps = fdroidserver.metadata.read_metadata()
knownapks = fdroidserver.common.KnownApks() knownapks = fdroidserver.common.KnownApks()
@ -348,7 +352,6 @@ class UpdateTest(unittest.TestCase):
fdroidserver.common.fill_config_defaults(config) fdroidserver.common.fill_config_defaults(config)
fdroidserver.common.config = config fdroidserver.common.config = config
fdroidserver.update.config = config fdroidserver.update.config = config
fdroidserver.update.options = fdroidserver.common.options
os.chdir(tmptestsdir) os.chdir(tmptestsdir)
apps = fdroidserver.metadata.read_metadata() apps = fdroidserver.metadata.read_metadata()
@ -381,7 +384,6 @@ class UpdateTest(unittest.TestCase):
fdroidserver.common.fill_config_defaults(config) fdroidserver.common.fill_config_defaults(config)
fdroidserver.common.config = config fdroidserver.common.config = config
fdroidserver.update.config = config fdroidserver.update.config = config
fdroidserver.update.options = fdroidserver.common.options
apps = fdroidserver.metadata.read_metadata() apps = fdroidserver.metadata.read_metadata()
self.assertTrue(packageName in apps) self.assertTrue(packageName in apps)
@ -483,6 +485,7 @@ class UpdateTest(unittest.TestCase):
# config needed to use jarsigner and keytool # config needed to use jarsigner and keytool
config = dict() config = dict()
fdroidserver.common.fill_config_defaults(config) fdroidserver.common.fill_config_defaults(config)
fdroidserver.common.options = Options
fdroidserver.update.config = config fdroidserver.update.config = config
apkfile = 'urzip.apk' apkfile = 'urzip.apk'
sig = self.javagetsig(apkfile) sig = self.javagetsig(apkfile)
@ -574,11 +577,10 @@ class UpdateTest(unittest.TestCase):
fdroidserver.common.config = config fdroidserver.common.config = config
fdroidserver.update.config = config fdroidserver.update.config = config
fdroidserver.update.options = type('', (), {})() fdroidserver.common.options = Options
fdroidserver.update.options = fdroidserver.common.options
fdroidserver.update.options.clean = True fdroidserver.update.options.clean = True
fdroidserver.update.options.delete_unknown = True fdroidserver.update.options.delete_unknown = True
fdroidserver.update.options.rename_apks = False
fdroidserver.update.options.allow_disabled_algorithms = False
apps = fdroidserver.metadata.read_metadata() apps = fdroidserver.metadata.read_metadata()
knownapks = fdroidserver.common.KnownApks() knownapks = fdroidserver.common.KnownApks()
@ -633,11 +635,10 @@ class UpdateTest(unittest.TestCase):
fdroidserver.common.config = config fdroidserver.common.config = config
fdroidserver.update.config = config fdroidserver.update.config = config
fdroidserver.update.options = type('', (), {})() fdroidserver.common.options = Options
fdroidserver.update.options = fdroidserver.common.options
fdroidserver.update.options.clean = True fdroidserver.update.options.clean = True
fdroidserver.update.options.delete_unknown = True fdroidserver.update.options.delete_unknown = True
fdroidserver.update.options.rename_apks = False
fdroidserver.update.options.allow_disabled_algorithms = False
fdroidserver.metadata.read_metadata() fdroidserver.metadata.read_metadata()
knownapks = fdroidserver.common.KnownApks() knownapks = fdroidserver.common.KnownApks()
@ -694,7 +695,7 @@ class UpdateTest(unittest.TestCase):
fdroidserver.common.fill_config_defaults(config) fdroidserver.common.fill_config_defaults(config)
fdroidserver.common.config = config fdroidserver.common.config = config
fdroidserver.update.config = config fdroidserver.update.config = config
fdroidserver.update.options = Options fdroidserver.common.options = Options
os.chdir(os.path.join(localmodule, 'tests')) os.chdir(os.path.join(localmodule, 'tests'))
apps = fdroidserver.metadata.read_metadata() apps = fdroidserver.metadata.read_metadata()
knownapks = fdroidserver.common.KnownApks() knownapks = fdroidserver.common.KnownApks()
@ -706,7 +707,7 @@ class UpdateTest(unittest.TestCase):
fdroidserver.common.fill_config_defaults(config) fdroidserver.common.fill_config_defaults(config)
fdroidserver.common.config = config fdroidserver.common.config = config
fdroidserver.update.config = config fdroidserver.update.config = config
fdroidserver.update.options = Options fdroidserver.common.options = Options
os.chdir(os.path.join(localmodule, 'tests')) os.chdir(os.path.join(localmodule, 'tests'))
apps = fdroidserver.metadata.read_metadata() apps = fdroidserver.metadata.read_metadata()
knownapks = fdroidserver.common.KnownApks() knownapks = fdroidserver.common.KnownApks()
@ -855,11 +856,11 @@ class UpdateTest(unittest.TestCase):
config['ndk_paths'] = dict() config['ndk_paths'] = dict()
fdroidserver.common.config = config fdroidserver.common.config = config
fdroidserver.update.config = config fdroidserver.update.config = config
fdroidserver.update.options = type('', (), {})()
fdroidserver.common.options = Options
fdroidserver.update.options = fdroidserver.common.options
fdroidserver.update.options.clean = True fdroidserver.update.options.clean = True
fdroidserver.update.options.rename_apks = False
fdroidserver.update.options.delete_unknown = True fdroidserver.update.options.delete_unknown = True
fdroidserver.update.options.allow_disabled_algorithms = False
for icon_dir in fdroidserver.update.get_all_icon_dirs('repo'): for icon_dir in fdroidserver.update.get_all_icon_dirs('repo'):
if not os.path.exists(icon_dir): if not os.path.exists(icon_dir):
@ -917,12 +918,11 @@ class UpdateTest(unittest.TestCase):
fdroidserver.common.config = config fdroidserver.common.config = config
fdroidserver.update.config = config fdroidserver.update.config = config
fdroidserver.update.options = type('', (), {})() fdroidserver.common.options = Options
fdroidserver.update.options = fdroidserver.common.options
fdroidserver.update.options.clean = True fdroidserver.update.options.clean = True
fdroidserver.update.options.verbose = True fdroidserver.update.options.verbose = True
fdroidserver.update.options.rename_apks = False
fdroidserver.update.options.delete_unknown = True fdroidserver.update.options.delete_unknown = True
fdroidserver.update.options.allow_disabled_algorithms = False
knownapks = fdroidserver.common.KnownApks() knownapks = fdroidserver.common.KnownApks()
@ -1014,6 +1014,7 @@ class UpdateTest(unittest.TestCase):
fdroidserver.common.fill_config_defaults(config) fdroidserver.common.fill_config_defaults(config)
fdroidserver.common.config = config fdroidserver.common.config = config
fdroidserver.update.config = config fdroidserver.update.config = config
fdroidserver.common.options = Options
fdroidserver.update.options = fdroidserver.common.options fdroidserver.update.options = fdroidserver.common.options
fdroidserver.update.options.delete_unknown = False fdroidserver.update.options.delete_unknown = False
@ -1026,6 +1027,104 @@ class UpdateTest(unittest.TestCase):
self.assertIsNone(apk) self.assertIsNone(apk)
self.assertFalse(cachechanged) self.assertFalse(cachechanged)
def test_get_apks_without_allowed_signatures(self):
"""Test when no AllowedAPKSigningKeys is specified"""
config = dict()
fdroidserver.common.fill_config_defaults(config)
fdroidserver.common.config = config
fdroidserver.common.options = Options
app = fdroidserver.metadata.App()
knownapks = fdroidserver.common.KnownApks()
apkfile = 'v1.v2.sig_1020.apk'
(skip, apk, cachechanged) = fdroidserver.update.process_apk(
{}, apkfile, 'repo', knownapks, False
)
r = fdroidserver.update.get_apks_without_allowed_signatures(app, apk)
self.assertIsNone(r)
def test_get_apks_without_allowed_signatures_allowed(self):
"""Test when the APK matches the specified AllowedAPKSigningKeys"""
config = dict()
fdroidserver.common.fill_config_defaults(config)
fdroidserver.common.config = config
fdroidserver.common.options = Options
fdroidserver.update.options = fdroidserver.common.options
app = fdroidserver.metadata.App(
{
'AllowedAPKSigningKeys': '32a23624c201b949f085996ba5ed53d40f703aca4989476949cae891022e0ed6'
}
)
knownapks = fdroidserver.common.KnownApks()
apkfile = 'v1.v2.sig_1020.apk'
(skip, apk, cachechanged) = fdroidserver.update.process_apk(
{}, apkfile, 'repo', knownapks, False
)
r = fdroidserver.update.get_apks_without_allowed_signatures(app, apk)
self.assertIsNone(r)
def test_get_apks_without_allowed_signatures_blocked(self):
"""Test when the APK does not match any specified AllowedAPKSigningKeys"""
config = dict()
fdroidserver.common.fill_config_defaults(config)
fdroidserver.common.config = config
fdroidserver.common.options = Options
fdroidserver.update.options = fdroidserver.common.options
app = fdroidserver.metadata.App(
{
'AllowedAPKSigningKeys': 'fa4edeadfa4edeadfa4edeadfa4edeadfa4edeadfa4edeadfa4edeadfa4edead'
}
)
knownapks = fdroidserver.common.KnownApks()
apkfile = 'v1.v2.sig_1020.apk'
(skip, apk, cachechanged) = fdroidserver.update.process_apk(
{}, apkfile, 'repo', knownapks, False
)
r = fdroidserver.update.get_apks_without_allowed_signatures(app, apk)
self.assertEqual(apkfile, r)
def test_update_with_AllowedAPKSigningKeys(self):
"""Test that APKs without allowed signatures get deleted."""
testdir = tempfile.mkdtemp(
prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir
)
os.chdir(testdir)
os.mkdir('repo')
testapk = os.path.join('repo', 'com.politedroid_6.apk')
shutil.copy(os.path.join(self.basedir, testapk), testapk)
os.mkdir('metadata')
metadatafile = os.path.join('metadata', 'com.politedroid.yml')
shutil.copy(os.path.join(self.basedir, metadatafile), metadatafile)
with open(metadatafile, 'a') as fp:
fp.write(
'\n\nAllowedAPKSigningKeys: 32a23624c201b949f085996ba5ed53d40f703aca4989476949cae891022e0ed6\n'
)
fdroidserver.common.options = Options
config = fdroidserver.common.read_config(fdroidserver.common.options)
config['repo_keyalias'] = 'sova'
config['keystorepass'] = 'r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI='
config['keypass'] = 'r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI='
config['keystore'] = os.path.join(self.basedir, 'keystore.jks')
self.assertTrue(os.path.exists(testapk))
with mock.patch('sys.argv', ['fdroid update', '--delete-unknown']):
fdroidserver.update.main()
self.assertTrue(os.path.exists(testapk))
with open(metadatafile, 'a') as fp:
fp.write(
'\n\nAllowedAPKSigningKeys: fa4edeadfa4edeadfa4edeadfa4edeadfa4edeadfa4edeadfa4edeadfa4edead\n'
)
with mock.patch('sys.argv', ['fdroid update', '--delete-unknown']):
fdroidserver.update.main()
self.assertFalse(os.path.exists(testapk))
def test_translate_per_build_anti_features(self): def test_translate_per_build_anti_features(self):
os.chdir(os.path.join(localmodule, 'tests')) os.chdir(os.path.join(localmodule, 'tests'))
testdir = tempfile.mkdtemp( testdir = tempfile.mkdtemp(
@ -1040,11 +1139,10 @@ class UpdateTest(unittest.TestCase):
fdroidserver.common.config = config fdroidserver.common.config = config
fdroidserver.update.config = config fdroidserver.update.config = config
fdroidserver.update.options = type('', (), {})() fdroidserver.common.options = Options
fdroidserver.update.options = fdroidserver.common.options
fdroidserver.update.options.clean = True fdroidserver.update.options.clean = True
fdroidserver.update.options.delete_unknown = True fdroidserver.update.options.delete_unknown = True
fdroidserver.update.options.rename_apks = False
fdroidserver.update.options.allow_disabled_algorithms = False
apps = fdroidserver.metadata.read_metadata() apps = fdroidserver.metadata.read_metadata()
knownapks = fdroidserver.common.KnownApks() knownapks = fdroidserver.common.KnownApks()
@ -1076,11 +1174,9 @@ class UpdateTest(unittest.TestCase):
fdroidserver.common.config = config fdroidserver.common.config = config
fdroidserver.update.config = config fdroidserver.update.config = config
fdroidserver.update.options = type('', (), {})() fdroidserver.common.options = Options
fdroidserver.update.options = fdroidserver.common.options
fdroidserver.update.options.clean = True fdroidserver.update.options.clean = True
fdroidserver.update.options.delete_unknown = False
fdroidserver.update.options.rename_apks = False
fdroidserver.update.options.allow_disabled_algorithms = False
knownapks = fdroidserver.common.KnownApks() knownapks = fdroidserver.common.KnownApks()
apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False) apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False)