From 746d4bd4cf33cc480e429d459f93c280a4306cf3 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 27 Jun 2017 21:40:39 +0200 Subject: [PATCH] update: allow_disabled_algorithms option to keep MD5 sigs in repo The new policy is to move APKs with invalid signatures to the archive, and only add those APKs to the archive's index if they have valid MD5 signatures. closes #323 closes #292 --- examples/config.py | 9 ++ fdroidserver/common.py | 1 + fdroidserver/update.py | 55 +++++++--- ...rg.bitbucket.tickytacky.mirrormirror_1.apk | Bin 0 -> 7173 bytes ...rg.bitbucket.tickytacky.mirrormirror_2.apk | Bin 0 -> 7084 bytes ...rg.bitbucket.tickytacky.mirrormirror_3.apk | Bin 0 -> 7126 bytes ...rg.bitbucket.tickytacky.mirrormirror_4.apk | Bin 0 -> 6588 bytes tests/run-tests | 100 ++++++++++++++++++ tests/update.TestCase | 85 ++++++++++++++- 9 files changed, 233 insertions(+), 17 deletions(-) create mode 100644 tests/org.bitbucket.tickytacky.mirrormirror_1.apk create mode 100644 tests/org.bitbucket.tickytacky.mirrormirror_2.apk create mode 100644 tests/org.bitbucket.tickytacky.mirrormirror_3.apk create mode 100644 tests/org.bitbucket.tickytacky.mirrormirror_4.apk diff --git a/examples/config.py b/examples/config.py index 0a0558b3..974f8ee1 100644 --- a/examples/config.py +++ b/examples/config.py @@ -71,6 +71,15 @@ archive_description = """ The repository of older versions of applications from the main demo repository. """ +# This allows a specific kind of insecure APK to be included in the +# 'repo' section. Since April 2017, APK signatures that use MD5 are +# no longer considered valid, jarsigner and apksigner will return an +# error when verifying. `fdroid update` will move APKs with these +# disabled signatures to the archive. This option stops that +# behavior, and lets those APKs stay part of 'repo'. +# +# allow_disabled_algorithms = True + # Normally, all apps are collected into a single app repository, like on # https://f-droid.org. For certain situations, it is better to make a repo # that is made up of APKs only from a single app. For example, an automated diff --git a/fdroidserver/common.py b/fdroidserver/common.py index 884fea4b..76888cce 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -85,6 +85,7 @@ default_config = { 'gradle': 'gradle', 'accepted_formats': ['txt', 'yml'], 'sync_from_local_copy_dir': False, + 'allow_disabled_algorithms': False, 'per_app_repos': False, 'make_current_version_link': True, 'current_version_name_source': 'Name', diff --git a/fdroidserver/update.py b/fdroidserver/update.py index 6ef9c126..8488574f 100644 --- a/fdroidserver/update.py +++ b/fdroidserver/update.py @@ -1082,7 +1082,8 @@ def scan_apk_androguard(apk, apkfile): apk['features'].append(feature) -def scan_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk): +def scan_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk=False, + allow_disabled_algorithms=False, archive_bad_sig=False): """Scan the apk with the given filename in the given repo directory. This also extracts the icons. @@ -1093,6 +1094,9 @@ def scan_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk): :param knownapks: known apks info :param use_date_from_apk: use date from APK (instead of current date) for newly added APKs + :param allow_disabled_algorithms: allow APKs with valid signatures that include + disabled algorithms in the signature (e.g. MD5) + :param archive_bad_sig: move APKs with a bad signature to the archive :returns: (skip, apk, cachechanged) where skip is a boolean indicating whether to skip this apk, apk is the scanned apk information, and cachechanged is True if the apkcache got changed. """ @@ -1186,19 +1190,27 @@ def scan_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk): # verify the jar signature is correct, allow deprecated # algorithms only if the APK is in the archive. + skipapk = False if not common.verify_apk_signature(apkfile): - if repodir == 'archive': + if repodir == 'archive' or allow_disabled_algorithms: if common.verify_old_apk_signature(apkfile): - apk['antiFeatures'].add('KnownVuln') + apk['antiFeatures'].update(['KnownVuln', 'DisabledAlgorithm']) else: - return True, None, False + skipapk = True else: - logging.warning('Archiving "' + apkfilename + '" with invalid signature!') - move_apk_between_sections('repo', 'archive', apk) - return True, None, False + skipapk = True - if has_known_vulnerability(apkfile): - apk['antiFeatures'].add('KnownVuln') + if skipapk: + if archive_bad_sig: + logging.warning('Archiving "' + apkfilename + '" with invalid signature!') + move_apk_between_sections(repodir, 'archive', apk) + else: + logging.warning('Skipping "' + apkfilename + '" with invalid signature!') + return True, None, False + + if 'KnownVuln' not in apk['antiFeatures']: + if has_known_vulnerability(apkfile): + apk['antiFeatures'].add('KnownVuln') apkzip = zipfile.ZipFile(apkfile, 'r') @@ -1372,7 +1384,9 @@ def scan_apks(apkcache, repodir, knownapks, use_date_from_apk=False): apks = [] for apkfile in sorted(glob.glob(os.path.join(repodir, '*.apk'))): apkfilename = apkfile[len(repodir) + 1:] - (skip, apk, cachechanged) = scan_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk) + ada = options.allow_disabled_algorithms or config['allow_disabled_algorithms'] + (skip, apk, cachechanged) = scan_apk(apkcache, apkfilename, repodir, knownapks, + use_date_from_apk, ada, True) if skip: continue apks.append(apk) @@ -1459,12 +1473,16 @@ def archive_old_apks(apps, apks, archapks, repodir, archivedir, defaultkeepversi current_app_archapks = filter_apk_list_sorted(archapks) if len(current_app_apks) < keepversions and len(current_app_archapks) > 0: - required = keepversions - len(apks) - # Move forward the ones we want again. - for apk in current_app_archapks[:required]: - move_apk_between_sections(archivedir, repodir, apk) - archapks.remove(apk) - apks.append(apk) + kept = 0 + # Move forward the ones we want again, except DisableAlgorithm + for apk in current_app_archapks: + if 'DisabledAlgorithm' not in apk['antiFeatures']: + move_apk_between_sections(archivedir, repodir, apk) + archapks.remove(apk) + apks.append(apk) + kept += 1 + if kept == keepversions: + break def move_apk_between_sections(from_dir, to_dir, apk): @@ -1477,6 +1495,9 @@ def move_apk_between_sections(from_dir, to_dir, apk): to_path = os.path.join(to_dir, filename) shutil.move(from_path, to_path) + if from_dir == to_dir: + return + logging.info("Moving %s from %s to %s" % (apk['apkName'], from_dir, to_dir)) _move_file(from_dir, to_dir, apk['apkName'], False) _move_file(from_dir, to_dir, apk['apkName'] + '.asc', True) @@ -1550,6 +1571,8 @@ def main(): help="Use date from apk instead of current time for newly added apks") parser.add_argument("--rename-apks", action="store_true", default=False, help="Rename APK files that do not match package.name_123.apk") + parser.add_argument("--allow-disabled-algorithms", action="store_true", default=False, + help="Include APKs that are signed with disabled algorithms like MD5") metadata.add_metadata_arguments(parser) options = parser.parse_args() metadata.warnings_action = options.W diff --git a/tests/org.bitbucket.tickytacky.mirrormirror_1.apk b/tests/org.bitbucket.tickytacky.mirrormirror_1.apk new file mode 100644 index 0000000000000000000000000000000000000000..6ec4272faeca8391875d168d658b70b4605cde7f GIT binary patch literal 7173 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 Sfr%Wv23*&HbUmCL+53M5B!<=i literal 0 HcmV?d00001 diff --git a/tests/org.bitbucket.tickytacky.mirrormirror_3.apk b/tests/org.bitbucket.tickytacky.mirrormirror_3.apk new file mode 100644 index 0000000000000000000000000000000000000000..af2a1fe839358efc1295aef2b9c897fe5190ca8e GIT binary patch literal 7126 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 config.py +echo 'keypass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py +echo "accepted_formats = ['txt']" >> config.py +sed -i '/allow_disabled_algorithms/d' config.py +test -d metadata || mkdir metadata +cp $WORKSPACE/tests/metadata/*.txt metadata/ +echo 'Summary:good test version of urzip' > metadata/info.guardianproject.urzip.txt +echo 'Summary:good MD5 sig, which is disabled algorithm' > metadata/org.bitbucket.tickytacky.mirrormirror.txt +sed -i '/Archive Policy:/d' metadata/*.txt +test -d repo || mkdir repo +cp $WORKSPACE/tests/urzip.apk \ + $WORKSPACE/tests/org.bitbucket.tickytacky.mirrormirror_[0-9].apk \ + $WORKSPACE/tests/repo/com.politedroid_[0-9].apk \ + $WORKSPACE/tests/repo/obb.main.twoversions_110161[357].apk \ + repo/ +sed -i 's,archive_older = [0-9],archive_older = 3,' config.py + +$fdroid update --pretty --nosign +test `grep '' archive/index.xml | wc -l` -eq 5 +test `grep '' repo/index.xml | wc -l` -eq 7 + + #------------------------------------------------------------------------------# echo_header 'test per-app "Archive Policy"' @@ -383,6 +412,77 @@ test -e repo/com.politedroid_5.apk ! test -e repo/com.politedroid_6.apk +#------------------------------------------------------------------------------# +echo_header 'test allowing disabled signatures in repo and archive' + +REPOROOT=`create_test_dir` +cd $REPOROOT +cp $WORKSPACE/tests/keystore.jks $REPOROOT/ +$fdroid init --keystore keystore.jks --repo-keyalias=sova +echo 'keystorepass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py +echo 'keypass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py +echo "accepted_formats = ['txt']" >> config.py +echo 'allow_disabled_algorithms = True' >> config.py +sed -i 's,archive_older = [0-9],archive_older = 3,' config.py +test -d metadata || mkdir metadata +cp $WORKSPACE/tests/metadata/com.politedroid.txt metadata/ +echo 'Summary:good test version of urzip' > metadata/info.guardianproject.urzip.txt +echo 'Summary:good MD5 sig, disabled algorithm' > metadata/org.bitbucket.tickytacky.mirrormirror.txt +sed -i '/Archive Policy:/d' metadata/*.txt +test -d repo || mkdir repo +cp $WORKSPACE/tests/repo/com.politedroid_[0-9].apk \ + $WORKSPACE/tests/org.bitbucket.tickytacky.mirrormirror_[0-9].apk \ + $WORKSPACE/tests/urzip-badsig.apk \ + repo/ + +$fdroid update --pretty --nosign +test `grep '' archive/index.xml | wc -l` -eq 2 +test `grep '' repo/index.xml | wc -l` -eq 6 +grep -F com.politedroid_3.apk archive/index.xml +grep -F com.politedroid_4.apk repo/index.xml +grep -F com.politedroid_5.apk repo/index.xml +grep -F com.politedroid_6.apk repo/index.xml +grep -F org.bitbucket.tickytacky.mirrormirror_1.apk archive/index.xml +grep -F org.bitbucket.tickytacky.mirrormirror_2.apk repo/index.xml +grep -F org.bitbucket.tickytacky.mirrormirror_3.apk repo/index.xml +grep -F org.bitbucket.tickytacky.mirrormirror_4.apk repo/index.xml +! grep -F urzip-badsig.apk repo/index.xml +! grep -F urzip-badsig.apk archive/index.xml +test -e archive/com.politedroid_3.apk +test -e repo/com.politedroid_4.apk +test -e repo/com.politedroid_5.apk +test -e repo/com.politedroid_6.apk +test -e archive/org.bitbucket.tickytacky.mirrormirror_1.apk +test -e repo/org.bitbucket.tickytacky.mirrormirror_2.apk +test -e repo/org.bitbucket.tickytacky.mirrormirror_3.apk +test -e repo/org.bitbucket.tickytacky.mirrormirror_4.apk +test -e archive/urzip-badsig.apk + +sed -i '/allow_disabled_algorithms/d' config.py +$fdroid update --pretty --nosign +test `grep '' archive/index.xml | wc -l` -eq 5 +test `grep '' repo/index.xml | wc -l` -eq 3 +grep -F org.bitbucket.tickytacky.mirrormirror_1.apk archive/index.xml +grep -F org.bitbucket.tickytacky.mirrormirror_2.apk archive/index.xml +grep -F org.bitbucket.tickytacky.mirrormirror_3.apk archive/index.xml +grep -F org.bitbucket.tickytacky.mirrormirror_4.apk archive/index.xml +grep -F com.politedroid_3.apk archive/index.xml +grep -F com.politedroid_4.apk repo/index.xml +grep -F com.politedroid_5.apk repo/index.xml +grep -F com.politedroid_6.apk repo/index.xml +! grep -F urzip-badsig.apk repo/index.xml +! grep -F urzip-badsig.apk archive/index.xml +test -e archive/org.bitbucket.tickytacky.mirrormirror_1.apk +test -e archive/org.bitbucket.tickytacky.mirrormirror_2.apk +test -e archive/org.bitbucket.tickytacky.mirrormirror_3.apk +test -e archive/org.bitbucket.tickytacky.mirrormirror_4.apk +test -e archive/com.politedroid_3.apk +test -e archive/urzip-badsig.apk +test -e repo/com.politedroid_4.apk +test -e repo/com.politedroid_5.apk +test -e repo/com.politedroid_6.apk + + #------------------------------------------------------------------------------# echo_header 'rename apks with `fdroid update --rename-apks`, --nosign for speed' diff --git a/tests/update.TestCase b/tests/update.TestCase index cd47e020..91406a35 100755 --- a/tests/update.TestCase +++ b/tests/update.TestCase @@ -201,6 +201,7 @@ class UpdateTest(unittest.TestCase): fdroidserver.update.options.clean = 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(xref=True) knownapks = fdroidserver.common.KnownApks() @@ -250,7 +251,7 @@ class UpdateTest(unittest.TestCase): config = dict() fdroidserver.common.fill_config_defaults(config) fdroidserver.update.config = config - os.chdir(os.path.dirname(__file__)) + os.chdir(os.path.join(localmodule, 'tests')) if os.path.basename(os.getcwd()) != 'tests': raise Exception('This test must be run in the "tests/" subdir') @@ -263,6 +264,7 @@ class UpdateTest(unittest.TestCase): fdroidserver.update.options.clean = True fdroidserver.update.options.rename_apks = False fdroidserver.update.options.delete_unknown = True + fdroidserver.update.options.allow_disabled_algorithms = False for icon_dir in fdroidserver.update.get_all_icon_dirs('repo'): if not os.path.exists(icon_dir): @@ -290,6 +292,87 @@ class UpdateTest(unittest.TestCase): self.maxDiff = None self.assertEqual(apk, frompickle) + def test_scan_apk_signed_by_disabled_algorithms(self): + os.chdir(os.path.join(localmodule, 'tests')) + if os.path.basename(os.getcwd()) != 'tests': + raise Exception('This test must be run in the "tests/" subdir') + + config = dict() + fdroidserver.common.fill_config_defaults(config) + fdroidserver.update.config = config + + config['ndk_paths'] = dict() + config['accepted_formats'] = ['json', 'txt', 'yml'] + fdroidserver.common.config = config + fdroidserver.update.config = config + + fdroidserver.update.options = type('', (), {})() + fdroidserver.update.options.clean = True + fdroidserver.update.options.verbose = True + fdroidserver.update.options.rename_apks = False + fdroidserver.update.options.delete_unknown = True + fdroidserver.update.options.allow_disabled_algorithms = False + + knownapks = fdroidserver.common.KnownApks() + apksourcedir = os.getcwd() + tmpdir = os.path.join(localmodule, '.testfiles') + if not os.path.exists(tmpdir): + os.makedirs(tmpdir) + tmptestsdir = tempfile.mkdtemp(prefix='test_scan_apk_signed_by_disabled_algorithms-', dir=tmpdir) + print('tmptestsdir', tmptestsdir) + os.chdir(tmptestsdir) + os.mkdir('repo') + os.mkdir('archive') + # setup the repo, create icons dirs, etc. + fdroidserver.update.scan_apks({}, 'repo', knownapks) + fdroidserver.update.scan_apks({}, 'archive', knownapks) + + disabledsigs = ['org.bitbucket.tickytacky.mirrormirror_2.apk', ] + for apkName in disabledsigs: + shutil.copy(os.path.join(apksourcedir, apkName), + os.path.join(tmptestsdir, 'repo')) + + skip, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'repo', knownapks, + allow_disabled_algorithms=True, + archive_bad_sig=False) + self.assertFalse(skip) + self.assertIsNotNone(apk) + self.assertTrue(cachechanged) + self.assertFalse(os.path.exists(os.path.join('archive', apkName))) + self.assertTrue(os.path.exists(os.path.join('repo', apkName))) + + # this test only works on systems with fully updated Java/jarsigner + # that has MD5 listed in jdk.jar.disabledAlgorithms in java.security + skip, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'repo', knownapks, + allow_disabled_algorithms=False, + archive_bad_sig=True) + self.assertTrue(skip) + self.assertIsNone(apk) + self.assertFalse(cachechanged) + self.assertTrue(os.path.exists(os.path.join('archive', apkName))) + self.assertFalse(os.path.exists(os.path.join('repo', apkName))) + + skip, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'archive', knownapks, + allow_disabled_algorithms=False, + archive_bad_sig=False) + self.assertFalse(skip) + self.assertIsNotNone(apk) + self.assertTrue(cachechanged) + self.assertTrue(os.path.exists(os.path.join('archive', apkName))) + self.assertFalse(os.path.exists(os.path.join('repo', apkName))) + + badsigs = ['urzip-badcert.apk', 'urzip-badsig.apk', 'urzip-release-unsigned.apk', ] + for apkName in badsigs: + shutil.copy(os.path.join(apksourcedir, apkName), + os.path.join(tmptestsdir, 'repo')) + + skip, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'repo', knownapks, + allow_disabled_algorithms=False, + archive_bad_sig=False) + self.assertTrue(skip) + self.assertIsNone(apk) + self.assertFalse(cachechanged) + def test_scan_invalid_apk(self): os.chdir(os.path.join(localmodule, 'tests')) if os.path.basename(os.getcwd()) != 'tests':