support APK Signature V2 when apksigner is installed
This was done with much help from @uniqx. This is the first level of supporting APK Signatures v1, v2, and v3. This is enough to include APKs with any combo of v1/v2/v3 signatures. For this to work at all, apksigner and androguard 3.3.3+ must be installed. closes #399
This commit is contained in:
parent
ea84014f9b
commit
d96f5ff660
|
@ -2516,28 +2516,45 @@ def signer_fingerprint(cert_encoded):
|
||||||
return hashlib.sha256(cert_encoded).hexdigest()
|
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):
|
def apk_signer_fingerprint(apk_path):
|
||||||
"""Obtain sha256 signing-key fingerprint for APK.
|
"""Obtain sha256 signing-key fingerprint for APK.
|
||||||
|
|
||||||
Extracts hexadecimal sha256 signing-key fingerprint string
|
Extracts hexadecimal sha256 signing-key fingerprint string
|
||||||
for a given APK.
|
for a given APK.
|
||||||
|
|
||||||
:param apkpath: path to APK
|
:param apk_path: path to APK
|
||||||
:returns: signature fingerprint
|
:returns: signature fingerprint
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with zipfile.ZipFile(apk_path, 'r') as apk:
|
cert_encoded = get_first_signer_certificate(apk_path)
|
||||||
certs = [n for n in apk.namelist() if SIGNATURE_BLOCK_FILE_REGEX.match(n)]
|
if not cert_encoded:
|
||||||
|
return None
|
||||||
if len(certs) < 1:
|
return signer_fingerprint(cert_encoded)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def apk_signer_fingerprint_short(apk_path):
|
def apk_signer_fingerprint_short(apk_path):
|
||||||
|
|
|
@ -414,29 +414,26 @@ def resize_all_icons(repodirs):
|
||||||
|
|
||||||
|
|
||||||
def getsig(apkpath):
|
def getsig(apkpath):
|
||||||
""" Get the signing certificate of an apk. To get the same md5 has that
|
"""Get the unique ID for the signing certificate of an APK.
|
||||||
Android gets, we encode the .RSA certificate in a specific format and pass
|
|
||||||
it hex-encoded to the md5 digest algorithm.
|
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
|
:param apkpath: path to the apk
|
||||||
:returns: A string containing the md5 of the signature of the apk or None
|
:returns: A string containing the md5 of the signature of the apk or None
|
||||||
if an error occurred.
|
if an error occurred.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with zipfile.ZipFile(apkpath, 'r') as apk:
|
cert_encoded = common.get_first_signer_certificate(apkpath)
|
||||||
certs = [n for n in apk.namelist() if common.SIGNATURE_BLOCK_FILE_REGEX.match(n)]
|
if not cert_encoded:
|
||||||
|
return None
|
||||||
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)
|
|
||||||
|
|
||||||
return hashlib.md5(hexlify(cert_encoded)).hexdigest() # nosec just used as ID for signing key
|
return hashlib.md5(hexlify(cert_encoded)).hexdigest() # nosec just used as ID for signing key
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -161,6 +161,7 @@ class CommonTest(unittest.TestCase):
|
||||||
testfiles = []
|
testfiles = []
|
||||||
testfiles.append(os.path.join(self.basedir, 'urzip-release.apk'))
|
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, 'urzip-release-unsigned.apk'))
|
||||||
|
testfiles.append(os.path.join(self.basedir, 'v2.only.sig_2.apk'))
|
||||||
for apkfile in testfiles:
|
for apkfile in testfiles:
|
||||||
debuggable = fdroidserver.common.is_apk_and_debuggable(apkfile)
|
debuggable = fdroidserver.common.is_apk_and_debuggable(apkfile)
|
||||||
self.assertFalse(debuggable,
|
self.assertFalse(debuggable,
|
||||||
|
|
Binary file not shown.
|
@ -14,6 +14,7 @@ import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
import yaml
|
import yaml
|
||||||
|
import zipfile
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
|
|
||||||
|
@ -233,6 +234,45 @@ class UpdateTest(unittest.TestCase):
|
||||||
self.assertEqual(sig, 'e0ecb5fc2d63088e4a07ae410a127722',
|
self.assertEqual(sig, 'e0ecb5fc2d63088e4a07ae410a127722',
|
||||||
"python sig should be: " + str(sig))
|
"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):
|
def testScanApksAndObbs(self):
|
||||||
os.chdir(os.path.join(localmodule, 'tests'))
|
os.chdir(os.path.join(localmodule, 'tests'))
|
||||||
if os.path.basename(os.getcwd()) != 'tests':
|
if os.path.basename(os.getcwd()) != 'tests':
|
||||||
|
@ -254,7 +294,7 @@ class UpdateTest(unittest.TestCase):
|
||||||
apps = fdroidserver.metadata.read_metadata(xref=True)
|
apps = fdroidserver.metadata.read_metadata(xref=True)
|
||||||
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)
|
||||||
self.assertEqual(len(apks), 16)
|
self.assertEqual(len(apks), 17)
|
||||||
apk = apks[1]
|
apk = apks[1]
|
||||||
self.assertEqual(apk['packageName'], 'com.politedroid')
|
self.assertEqual(apk['packageName'], 'com.politedroid')
|
||||||
self.assertEqual(apk['versionCode'], 3)
|
self.assertEqual(apk['versionCode'], 3)
|
||||||
|
@ -321,7 +361,7 @@ class UpdateTest(unittest.TestCase):
|
||||||
|
|
||||||
fdroidserver.update.options.clean = False
|
fdroidserver.update.options.clean = False
|
||||||
read_from_json = fdroidserver.update.get_cache()
|
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'):
|
for f in glob.glob('repo/*.apk'):
|
||||||
self.assertTrue(os.path.basename(f) in read_from_json)
|
self.assertTrue(os.path.basename(f) in read_from_json)
|
||||||
|
|
||||||
|
@ -363,6 +403,16 @@ class UpdateTest(unittest.TestCase):
|
||||||
else:
|
else:
|
||||||
continue
|
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')
|
apk_info = fdroidserver.update.scan_apk('repo/souch.smsbypass_9.apk')
|
||||||
self.assertIsNone(apk_info.get('maxSdkVersion'))
|
self.assertIsNone(apk_info.get('maxSdkVersion'))
|
||||||
self.assertEqual(apk_info.get('versionName'), '0.9')
|
self.assertEqual(apk_info.get('versionName'), '0.9')
|
||||||
|
@ -623,7 +673,7 @@ class UpdateTest(unittest.TestCase):
|
||||||
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)
|
||||||
fdroidserver.update.translate_per_build_anti_features(apps, apks)
|
fdroidserver.update.translate_per_build_anti_features(apps, apks)
|
||||||
self.assertEqual(len(apks), 16)
|
self.assertEqual(len(apks), 17)
|
||||||
foundtest = False
|
foundtest = False
|
||||||
for apk in apks:
|
for apk in apks:
|
||||||
if apk['packageName'] == 'com.politedroid' and apk['versionCode'] == 3:
|
if apk['packageName'] == 'com.politedroid' and apk['versionCode'] == 3:
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue