From efb0bf6ee50db8b578a5c76ca434967a3e531fe7 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Michael=20P=C3=B6hn?= Date: Sat, 23 Sep 2017 09:36:22 +0200 Subject: [PATCH] add common functions for dealing with apk signatures --- fdroidserver/common.py | 113 +++++++++++++++++++++++++++++++++++++++++ tests/common.TestCase | 11 ++++ 2 files changed, 124 insertions(+) diff --git a/fdroidserver/common.py b/fdroidserver/common.py index 887203f3..c3c3534c 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -35,6 +35,7 @@ import hashlib import socket import base64 import zipfile +import tempfile import xml.etree.ElementTree as XMLElementTree from binascii import hexlify @@ -2015,6 +2016,18 @@ def place_srclib(root_dir, number, libpath): apk_sigfile = re.compile(r'META-INF/[0-9A-Za-z]+\.(SF|RSA|DSA|EC)') +def signer_fingerprint_short(sig): + """Obtain shortened sha256 signing-key fingerprint for pkcs7 signature. + + Extracts the first 7 hexadecimal digits of sha256 signing-key fingerprint + for a given pkcs7 signature. + + :param sig: Contents of an APK signature. + :returns: shortened signing-key fingerprint. + """ + return signer_fingerprint(sig)[:7] + + def signer_fingerprint(sig): """Obtain sha256 signing-key fingerprint for pkcs7 signature. @@ -2052,6 +2065,18 @@ def apk_signer_fingerprint(apk_path): return signer_fingerprint(cert) +def apk_signer_fingerprint_short(apk_path): + """Obtain shortened sha256 signing-key fingerprint for APK. + + Extracts the first 7 hexadecimal digits of sha256 signing-key fingerprint + for a given pkcs7 APK. + + :param apk_path: path to APK + :returns: shortened signing-key fingerprint + """ + return apk_signer_fingerprint(apk_path)[:7] + + def metadata_get_sigdir(appid, vercode=None): """Get signature directory for app""" if vercode: @@ -2060,6 +2085,94 @@ def metadata_get_sigdir(appid, vercode=None): return os.path.join('metadata', appid, 'signatures') +def metadata_find_signing_files(appid, vercode): + """Gets a list of singed manifests and signatures. + + :param appid: id string of that app + :param vercode: version code of that app + :returns: a list of triplets for each signing key with following paths: + (signature_file, singed_file, manifest_file) + """ + ret = [] + sigdir = metadata_get_sigdir(appid, vercode) + sigs = glob.glob(os.path.join(sigdir, '*.DSA')) + \ + glob.glob(os.path.join(sigdir, '*.EC')) + \ + glob.glob(os.path.join(sigdir, '*.RSA')) + extre = re.compile('(\.DSA|\.EC|\.RSA)$') + for sig in sigs: + sf = extre.sub('.SF', sig) + if os.path.isfile(sf): + mf = os.path.join(sigdir, 'MANIFEST.MF') + if os.path.isfile(mf): + ret.append((sig, sf, mf)) + return ret + + +def metadata_find_developer_signing_files(appid, vercode): + """Get developer signature files for specified app from metadata. + + :returns: A triplet of paths for signing files from metadata: + (signature_file, singed_file, manifest_file) + """ + allsigningfiles = metadata_find_signing_files(appid, vercode) + if allsigningfiles and len(allsigningfiles) == 1: + return allsigningfiles[0] + else: + return None + + +def apk_strip_signatures(signed_apk, strip_manifest=False): + """Removes signatures from APK. + + :param signed_apk: path to apk file. + :param strip_manifest: when set to True also the manifest file will + be removed from the APK. + """ + with tempfile.TemporaryDirectory() as tmpdir: + tmp_apk = os.path.join(tmpdir, 'tmp.apk') + os.rename(signed_apk, tmp_apk) + with ZipFile(tmp_apk, 'r') as in_apk: + with ZipFile(signed_apk, 'w') as out_apk: + for f in in_apk.infolist(): + if not apk_sigfile.match(f.filename): + if strip_manifest: + if f.filename != 'META-INF/MANIFEST.MF': + buf = in_apk.read(f.filename) + out_apk.writestr(f.filename, buf) + else: + buf = in_apk.read(f.filename) + out_apk.writestr(f.filename, buf) + + +def apk_implant_signatures(apkpath, signaturefile, signedfile, manifest): + """Implats a signature from out metadata into an APK. + + Note: this changes there supplied APK in place. So copy it if you + need the original to be preserved. + + :param apkpath: location of the apk + """ + # get list of available signature files in metadata + with tempfile.TemporaryDirectory() as tmpdir: + # orig_apk = os.path.join(tmpdir, 'orig.apk') + # os.rename(apkpath, orig_apk) + apkwithnewsig = os.path.join(tmpdir, 'newsig.apk') + with ZipFile(apkpath, 'r') as in_apk: + with ZipFile(apkwithnewsig, 'w') as out_apk: + for sig_file in [signaturefile, signedfile, manifest]: + out_apk.write(sig_file, arcname='META-INF/' + + os.path.basename(sig_file)) + for f in in_apk.infolist(): + if not apk_sigfile.match(f.filename): + if f.filename != 'META-INF/MANIFEST.MF': + buf = in_apk.read(f.filename) + out_apk.writestr(f.filename, buf) + os.remove(apkpath) + p = SdkToolsPopen(['zipalign', '-v', '4', apkwithnewsig, apkpath]) + if p.returncode != 0: + raise BuildException("Failed to align application") + + def apk_extract_signatures(apkpath, outdir, manifest=True): """Extracts a signature files from APK and puts them into target directory. diff --git a/tests/common.TestCase b/tests/common.TestCase index dee8b667..623bb76d 100755 --- a/tests/common.TestCase +++ b/tests/common.TestCase @@ -390,6 +390,17 @@ class CommonTest(unittest.TestCase): self.assertEqual(keytoolcertfingerprint, fdroidserver.common.apk_signer_fingerprint(apkfile)) + def test_apk_signer_fingerprint_short(self): + + # fingerprints fetched with: keytool -printcert -file ____.RSA + testapks = (('repo/obb.main.oldversion_1444412523.apk', '818e469'), + ('repo/obb.main.twoversions_1101613.apk', '32a2362'), + ('repo/obb.main.twoversions_1101617.apk', '32a2362')) + + for apkfile, keytoolcertfingerprint in testapks: + self.assertEqual(keytoolcertfingerprint, + fdroidserver.common.apk_signer_fingerprint_short(apkfile)) + if __name__ == "__main__": parser = optparse.OptionParser() -- 2.30.2