diff --git a/fdroidserver/common.py b/fdroidserver/common.py index 701a9e03..c362049e 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -2516,28 +2516,45 @@ def signer_fingerprint(cert_encoded): return hashlib.sha256(cert_encoded).hexdigest() +def get_first_signer_certificate(apkpath): + """Get the first signing certificate from the APK, DER-encoded""" + certs = None + cert_encoded = None + with zipfile.ZipFile(apkpath, 'r') as apk: + cert_files = [n for n in apk.namelist() if SIGNATURE_BLOCK_FILE_REGEX.match(n)] + if len(cert_files) > 1: + logging.error(_("Found multiple JAR Signature Block Files in {path}").format(path=apkpath)) + return None + elif len(cert_files) == 1: + cert_encoded = get_certificate(apk.read(cert_files[0])) + + if cert_encoded is None: + apkobject = _get_androguard_APK(apkpath) + certs = apkobject.get_certificates_der_v2() + if len(certs) > 0: + logging.info(_('Using APK v2 Signature')) + cert_encoded = certs[0] + + if not cert_encoded: + logging.error(_("No signing certificates found in {path}").format(path=apkpath)) + return None + return cert_encoded + + def apk_signer_fingerprint(apk_path): """Obtain sha256 signing-key fingerprint for APK. Extracts hexadecimal sha256 signing-key fingerprint string for a given APK. - :param apkpath: path to APK + :param apk_path: path to APK :returns: signature fingerprint """ - with zipfile.ZipFile(apk_path, 'r') as apk: - certs = [n for n in apk.namelist() if SIGNATURE_BLOCK_FILE_REGEX.match(n)] - - if len(certs) < 1: - logging.error("Found no signing certificates on %s" % apk_path) - return None - if len(certs) > 1: - logging.error("Found multiple signing certificates on %s" % apk_path) - return None - - cert_encoded = get_certificate(apk.read(certs[0])) - return signer_fingerprint(cert_encoded) + cert_encoded = get_first_signer_certificate(apk_path) + if not cert_encoded: + return None + return signer_fingerprint(cert_encoded) def apk_signer_fingerprint_short(apk_path): diff --git a/fdroidserver/update.py b/fdroidserver/update.py index 42be7e5c..a0f9a5f2 100644 --- a/fdroidserver/update.py +++ b/fdroidserver/update.py @@ -414,29 +414,26 @@ def resize_all_icons(repodirs): def getsig(apkpath): - """ Get the signing certificate of an apk. To get the same md5 has that - Android gets, we encode the .RSA certificate in a specific format and pass - it hex-encoded to the md5 digest algorithm. + """Get the unique ID for the signing certificate of an APK. + + This uses a strange algorithm that was devised at the very + beginning of F-Droid. Since it is only used for checking + signature compatibility, it does not matter much that it uses MD5. + + To get the same MD5 has that fdroidclient gets, we encode the .RSA + certificate in a specific format and pass it hex-encoded to the + md5 digest algorithm. This is not the same as the standard X.509 + certificate fingerprint. :param apkpath: path to the apk :returns: A string containing the md5 of the signature of the apk or None if an error occurred. + """ - with zipfile.ZipFile(apkpath, 'r') as apk: - certs = [n for n in apk.namelist() if common.SIGNATURE_BLOCK_FILE_REGEX.match(n)] - - if len(certs) < 1: - logging.error(_("No signing certificates found in {path}").format(path=apkpath)) - return None - if len(certs) > 1: - logging.error(_("Found multiple signing certificates in {path}").format(path=apkpath)) - return None - - cert = apk.read(certs[0]) - - cert_encoded = common.get_certificate(cert) - + cert_encoded = common.get_first_signer_certificate(apkpath) + if not cert_encoded: + return None return hashlib.md5(hexlify(cert_encoded)).hexdigest() # nosec just used as ID for signing key diff --git a/tests/common.TestCase b/tests/common.TestCase index 6c5ba647..b976376f 100755 --- a/tests/common.TestCase +++ b/tests/common.TestCase @@ -161,6 +161,7 @@ class CommonTest(unittest.TestCase): testfiles = [] testfiles.append(os.path.join(self.basedir, 'urzip-release.apk')) testfiles.append(os.path.join(self.basedir, 'urzip-release-unsigned.apk')) + testfiles.append(os.path.join(self.basedir, 'v2.only.sig_2.apk')) for apkfile in testfiles: debuggable = fdroidserver.common.is_apk_and_debuggable(apkfile) self.assertFalse(debuggable, diff --git a/tests/repo/v1.v2.sig_1020.apk b/tests/repo/v1.v2.sig_1020.apk new file mode 100644 index 00000000..006ff764 Binary files /dev/null and b/tests/repo/v1.v2.sig_1020.apk differ diff --git a/tests/update.TestCase b/tests/update.TestCase index 80870e02..2ba207b8 100755 --- a/tests/update.TestCase +++ b/tests/update.TestCase @@ -14,6 +14,7 @@ import sys import tempfile import unittest import yaml +import zipfile from binascii import unhexlify from distutils.version import LooseVersion @@ -233,6 +234,45 @@ class UpdateTest(unittest.TestCase): self.assertEqual(sig, 'e0ecb5fc2d63088e4a07ae410a127722', "python sig should be: " + str(sig)) + def test_getsig(self): + # config needed to use jarsigner and keytool + config = dict() + fdroidserver.common.fill_config_defaults(config) + fdroidserver.update.config = config + + sig = fdroidserver.update.getsig('urzip-release-unsigned.apk') + self.assertIsNone(sig) + + good_fingerprint = 'b4964fd759edaa54e65bb476d0276880' + + apkpath = 'urzip-release.apk' # v1 only + sig = fdroidserver.update.getsig(apkpath) + self.assertEqual(good_fingerprint, sig, + 'python sig was: ' + str(sig)) + + apkpath = 'repo/v1.v2.sig_1020.apk' + sig = fdroidserver.update.getsig(apkpath) + self.assertEqual(good_fingerprint, sig, + 'python sig was: ' + str(sig)) + # check that v1 and v2 have the same certificate + import hashlib + from binascii import hexlify + from androguard.core.bytecodes.apk import APK + apkobject = APK(apkpath) + cert_encoded = apkobject.get_certificates_der_v2()[0] + self.assertEqual(good_fingerprint, sig, + hashlib.md5(hexlify(cert_encoded)).hexdigest()) # nosec just used as ID for signing key + + filename = 'v2.only.sig_2.apk' + with zipfile.ZipFile(filename) as z: + self.assertTrue('META-INF/MANIFEST.MF' in z.namelist(), 'META-INF/MANIFEST.MF required') + for f in z.namelist(): + # ensure there are no v1 signature files + self.assertIsNone(fdroidserver.common.SIGNATURE_BLOCK_FILE_REGEX.match(f)) + sig = fdroidserver.update.getsig(filename) + self.assertEqual(good_fingerprint, sig, + "python sig was: " + str(sig)) + def testScanApksAndObbs(self): os.chdir(os.path.join(localmodule, 'tests')) if os.path.basename(os.getcwd()) != 'tests': @@ -254,7 +294,7 @@ class UpdateTest(unittest.TestCase): apps = fdroidserver.metadata.read_metadata(xref=True) knownapks = fdroidserver.common.KnownApks() apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False) - self.assertEqual(len(apks), 16) + self.assertEqual(len(apks), 17) apk = apks[1] self.assertEqual(apk['packageName'], 'com.politedroid') self.assertEqual(apk['versionCode'], 3) @@ -321,7 +361,7 @@ class UpdateTest(unittest.TestCase): fdroidserver.update.options.clean = False read_from_json = fdroidserver.update.get_cache() - self.assertEqual(18, len(read_from_json)) + self.assertEqual(19, len(read_from_json)) for f in glob.glob('repo/*.apk'): self.assertTrue(os.path.basename(f) in read_from_json) @@ -363,6 +403,16 @@ class UpdateTest(unittest.TestCase): else: continue + apk_info = fdroidserver.update.scan_apk('repo/v1.v2.sig_1020.apk') + self.assertIsNone(apk_info.get('maxSdkVersion')) + self.assertEqual(apk_info.get('versionName'), 'v1+2') + self.assertEqual(apk_info.get('versionCode'), 1020) + + apk_info = fdroidserver.update.scan_apk('v2.only.sig_2.apk') + self.assertIsNone(apk_info.get('maxSdkVersion')) + self.assertEqual(apk_info.get('versionName'), 'v2-only') + self.assertEqual(apk_info.get('versionCode'), 2) + apk_info = fdroidserver.update.scan_apk('repo/souch.smsbypass_9.apk') self.assertIsNone(apk_info.get('maxSdkVersion')) self.assertEqual(apk_info.get('versionName'), '0.9') @@ -623,7 +673,7 @@ class UpdateTest(unittest.TestCase): knownapks = fdroidserver.common.KnownApks() apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False) fdroidserver.update.translate_per_build_anti_features(apps, apks) - self.assertEqual(len(apks), 16) + self.assertEqual(len(apks), 17) foundtest = False for apk in apks: if apk['packageName'] == 'com.politedroid' and apk['versionCode'] == 3: diff --git a/tests/v2.only.sig_2.apk b/tests/v2.only.sig_2.apk new file mode 100644 index 00000000..0b1804d3 Binary files /dev/null and b/tests/v2.only.sig_2.apk differ