chiark / gitweb /
add common.sign_apk() for nighly as test for using in publish
authorHans-Christoph Steiner <hans@eds.org>
Thu, 30 Nov 2017 20:10:41 +0000 (21:10 +0100)
committerHans-Christoph Steiner <hans@eds.org>
Mon, 4 Dec 2017 21:52:41 +0000 (22:52 +0100)
Since the MD5 migration was quite a bit of work, it makes sense to start
on moving away from SHA1 as much as possible while it is easy to do. SHA256
will only work in APK signatures on android-18 (4.3) or newer.  So if an
APK has a minSdkVersion of 18 or newer, then sign with SHA256.

https://issuetracker.google.com/issues/36956587
https://android-review.googlesource.com/c/platform/libcore/+/44491

fdroidserver/common.py
fdroidserver/publish.py
tests/common.TestCase

index 9e19934df2fe18c387ab23ae5ca0b32d0e205b9e..24c695e2fbd74e4f568ae30d4187f5652e7702ca 100644 (file)
@@ -1939,6 +1939,22 @@ def get_apk_id_aapt(apkfile):
                           .format(apkfilename=apkfile))
 
 
+def get_minSdkVersion_aapt(apkfile):
+    """Extract the minimum supported Android SDK from an APK using aapt
+
+    :param apkfile: path to an APK file.
+    :returns: the integer representing the SDK version
+    """
+    r = re.compile(r"^sdkVersion:'([0-9]+)'")
+    p = SdkToolsPopen(['aapt', 'dump', 'badging', apkfile], output=False)
+    for line in p.output.splitlines():
+        m = r.match(line)
+        if m:
+            return int(m.group(1))
+    raise FDroidException(_('Reading minSdkVersion failed: "{apkfilename}"')
+                          .format(apkfilename=apkfile))
+
+
 class PopenResult:
     def __init__(self):
         self.returncode = None
@@ -2413,6 +2429,40 @@ def apk_extract_signatures(apkpath, outdir, manifest=True):
                     out_file.write(in_apk.read(f.filename))
 
 
+def sign_apk(unsigned_path, signed_path, keyalias):
+    """Sign and zipalign an unsigned APK, then save to a new file, deleting the unsigned
+
+    android-18 (4.3) finally added support for reasonable hash
+    algorithms, like SHA-256, before then, the only options were MD5
+    and SHA1 :-/ This aims to use SHA-256 when the APK does not target
+    older Android versions, and is therefore safe to do so.
+
+    https://issuetracker.google.com/issues/36956587
+    https://android-review.googlesource.com/c/platform/libcore/+/44491
+
+    """
+
+    if get_minSdkVersion_aapt(unsigned_path) < 18:
+        signature_algorithm = ['-sigalg', 'SHA1withRSA', '-digestalg', 'SHA1']
+    else:
+        signature_algorithm = ['-sigalg', 'SHA256withRSA', '-digestalg', 'SHA256']
+
+    p = FDroidPopen([config['jarsigner'], '-keystore', config['keystore'],
+                     '-storepass:env', 'FDROID_KEY_STORE_PASS',
+                     '-keypass:env', 'FDROID_KEY_PASS']
+                    + signature_algorithm + [unsigned_path, keyalias],
+                    envs={
+                        'FDROID_KEY_STORE_PASS': config['keystorepass'],
+                        'FDROID_KEY_PASS': config['keypass'], })
+    if p.returncode != 0:
+        raise BuildException(_("Failed to sign application"), p.output)
+
+    p = SdkToolsPopen(['zipalign', '-v', '4', unsigned_path, signed_path])
+    if p.returncode != 0:
+        raise BuildException(_("Failed to zipalign application"))
+    os.remove(unsigned_path)
+
+
 def verify_apks(signed_apk, unsigned_apk, tmp_dir):
     """Verify that two apks are the same
 
index 431d2aefa0f562395bc3966ceaa75c3afd074fce..d50bfaa14dcfd711e31e34371f74c1be9f1c12b1 100644 (file)
@@ -339,6 +339,7 @@ def main():
                                                                       unsigned_dir,
                                                                       output_dir))
 
+                # TODO replace below with common.sign_apk() once it has proven stable
                 # Sign the application...
                 p = FDroidPopen([config['jarsigner'], '-keystore', config['keystore'],
                                  '-storepass:env', 'FDROID_KEY_STORE_PASS',
index dfb4380a23e9317d0e8ff4fff9dbc9741c7300f1..0ed9ced9f099e33519504414d2a1e11fa433dd28 100755 (executable)
@@ -456,6 +456,29 @@ class CommonTest(unittest.TestCase):
             self.assertEqual(keytoolcertfingerprint,
                              fdroidserver.common.apk_signer_fingerprint_short(apkfile))
 
+    def test_sign_apk(self):
+        fdroidserver.common.config = None
+        config = fdroidserver.common.read_config(fdroidserver.common.options)
+        config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
+        config['keyalias'] = 'sova'
+        config['keystorepass'] = 'r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI='
+        config['keypass'] = 'r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI='
+        config['keystore'] = os.path.join(self.basedir, 'keystore.jks')
+        fdroidserver.common.config = config
+        fdroidserver.signindex.config = config
+
+        testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir)
+        unsigned = os.path.join(testdir, 'urzip-release-unsigned.apk')
+        signed = os.path.join(testdir, 'urzip-release.apk')
+
+        self.assertFalse(fdroidserver.common.verify_apk_signature(unsigned))
+
+        shutil.copy(os.path.join(self.basedir, 'urzip-release-unsigned.apk'), testdir)
+        fdroidserver.common.sign_apk(unsigned, signed, config['keyalias'])
+        self.assertTrue(os.path.isfile(signed))
+        self.assertFalse(os.path.isfile(unsigned))
+        self.assertTrue(fdroidserver.common.verify_apk_signature(signed))
+
     def test_get_api_id_aapt(self):
 
         config = dict()
@@ -472,6 +495,61 @@ class CommonTest(unittest.TestCase):
         with self.assertRaises(FDroidException):
             fdroidserver.common.get_apk_id_aapt('nope')
 
+    def test_get_minSdkVersion_aapt(self):
+
+        config = dict()
+        fdroidserver.common.fill_config_defaults(config)
+        fdroidserver.common.config = config
+        self._set_build_tools()
+        config['aapt'] = fdroidserver.common.find_sdk_tools_cmd('aapt')
+
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('bad-unicode-πÇÇ现代通用字-български-عربي1.apk')
+        self.assertEqual(4, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.bitbucket.tickytacky.mirrormirror_1.apk')
+        self.assertEqual(14, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.bitbucket.tickytacky.mirrormirror_2.apk')
+        self.assertEqual(14, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.bitbucket.tickytacky.mirrormirror_3.apk')
+        self.assertEqual(14, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.bitbucket.tickytacky.mirrormirror_4.apk')
+        self.assertEqual(14, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.dyndns.fules.ck_20.apk')
+        self.assertEqual(7, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip.apk')
+        self.assertEqual(4, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip-badcert.apk')
+        self.assertEqual(4, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip-badsig.apk')
+        self.assertEqual(4, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip-release.apk')
+        self.assertEqual(4, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip-release-unsigned.apk')
+        self.assertEqual(4, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/com.politedroid_3.apk')
+        self.assertEqual(3, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/com.politedroid_4.apk')
+        self.assertEqual(3, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/com.politedroid_5.apk')
+        self.assertEqual(3, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/com.politedroid_6.apk')
+        self.assertEqual(14, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.main.oldversion_1444412523.apk')
+        self.assertEqual(4, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.mainpatch.current_1619_another-release-key.apk')
+        self.assertEqual(4, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.mainpatch.current_1619.apk')
+        self.assertEqual(4, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.main.twoversions_1101613.apk')
+        self.assertEqual(4, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.main.twoversions_1101615.apk')
+        self.assertEqual(4, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.main.twoversions_1101617.apk')
+        self.assertEqual(4, minSdkVersion)
+        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/urzip-; Рахма́нинов, [rɐxˈmanʲɪnəf] سيرجي_رخمانينوف 谢尔盖·.apk')
+
+        with self.assertRaises(FDroidException):
+            fdroidserver.common.get_minSdkVersion_aapt('nope')
+
     def test_apk_release_name(self):
         appid, vercode, sigfp = fdroidserver.common.apk_parse_release_filename('com.serwylo.lexica_905.apk')
         self.assertEqual(appid, 'com.serwylo.lexica')