1
0
mirror of https://gitlab.com/fdroid/fdroidserver.git synced 2024-11-13 10:30:13 +01:00

Merge branch 'v2-sig-support' into 'master'

first basic support for APK Signature v2 and v3

See merge request fdroid/fdroidserver!618
This commit is contained in:
Hans-Christoph Steiner 2019-02-01 08:40:20 +00:00
commit 64bab7a94c
9 changed files with 124 additions and 52 deletions

View File

@ -90,8 +90,7 @@ __complete_options() {
__complete_build() {
opts="-v -q -l -s -t -f -a -w"
lopts="--verbose --quiet --latest --stop --test --server --reset-server
--on-server --skip-scan --no-tarball --force --all --wiki --no-refresh"
lopts="--verbose --quiet --latest --stop --test --server --reset-server --skip-scan --no-tarball --force --all --wiki --no-refresh"
case "${cur}" in
-*)
__complete_options

View File

@ -72,7 +72,7 @@ MINIMUM_AAPT_VERSION = '26.0.0'
VERCODE_OPERATION_RE = re.compile(r'^([ 0-9/*+-]|%c)+$')
# A signature block file with a .DSA, .RSA, or .EC extension
CERT_PATH_REGEX = re.compile(r'^META-INF/.*\.(DSA|EC|RSA)$')
SIGNATURE_BLOCK_FILE_REGEX = re.compile(r'^META-INF/.*\.(DSA|EC|RSA)$')
APK_NAME_REGEX = re.compile(r'^([a-zA-Z][\w.]*)_(-?[0-9]+)_?([0-9a-f]{7})?\.apk')
APK_ID_TRIPLET_REGEX = re.compile(r"^package: name='(\w[^']*)' versionCode='([^']+)' versionName='([^']*)'")
STANDARD_FILE_NAME_REGEX = re.compile(r'^(\w[\w.]*)_(-?[0-9]+)\.\w+')
@ -2492,20 +2492,20 @@ def place_srclib(root_dir, number, libpath):
APK_SIGNATURE_FILES = 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.
def signer_fingerprint_short(cert_encoded):
"""Obtain shortened sha256 signing-key fingerprint for pkcs7 DER certficate.
Extracts the first 7 hexadecimal digits of sha256 signing-key fingerprint
for a given pkcs7 signature.
:param sig: Contents of an APK signing certificate.
:param cert_encoded: Contents of an APK signing certificate.
:returns: shortened signing-key fingerprint.
"""
return signer_fingerprint(sig)[:7]
return signer_fingerprint(cert_encoded)[:7]
def signer_fingerprint(sig):
"""Obtain sha256 signing-key fingerprint for pkcs7 signature.
def signer_fingerprint(cert_encoded):
"""Obtain sha256 signing-key fingerprint for pkcs7 DER certificate.
Extracts hexadecimal sha256 signing-key fingerprint string
for a given pkcs7 signature.
@ -2513,32 +2513,53 @@ def signer_fingerprint(sig):
:param: Contents of an APK signature.
:returns: shortened signature fingerprint.
"""
cert_encoded = get_certificate(sig)
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 not cert_encoded:
apkobject = _get_androguard_APK(apkpath)
certs = apkobject.get_certificates_der_v2()
if len(certs) > 0:
logging.info(_('Using APK Signature v2'))
cert_encoded = certs[0]
if not cert_encoded:
certs = apkobject.get_certificates_der_v3()
if len(certs) > 0:
logging.info(_('Using APK Signature v3'))
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 CERT_PATH_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 = apk.read(certs[0])
return signer_fingerprint(cert)
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):
@ -2591,7 +2612,7 @@ def metadata_find_developer_signature(appid, vercode=None):
raise FDroidException('ambiguous signatures, please make sure there is only one signature in \'{}\'. (The signature has to be the App maintainers signature for version of the APK.)'.format(sigdir))
for sig in sigs:
with open(sig, 'rb') as f:
return signer_fingerprint(f.read())
return signer_fingerprint(get_certificate(f.read()))
return None
@ -3071,13 +3092,17 @@ def get_cert_fingerprint(pubkey):
return " ".join(ret)
def get_certificate(certificate_file):
def get_certificate(signature_block_file):
"""Extracts a DER certificate from JAR Signature's "Signature Block File".
:param signature_block_file: file bytes (as string) representing the
certificate, as read directly out of the APK/ZIP
:return: A binary representation of the certificate's public key,
or None in case of error
"""
Extracts a certificate from the given file.
:param certificate_file: file bytes (as string) representing the certificate
:return: A binary representation of the certificate's public key, or None in case of error
"""
content = decoder.decode(certificate_file, asn1Spec=rfc2315.ContentInfo())[0]
content = decoder.decode(signature_block_file, asn1Spec=rfc2315.ContentInfo())[0]
if content.getComponentByName('contentType') != rfc2315.signedData:
return None
content = decoder.decode(content.getComponentByName('content'),

View File

@ -751,7 +751,7 @@ def get_public_key_from_jar(jar):
:return: the public key from the jar and its fingerprint
"""
# extract certificate from jar
certs = [n for n in jar.namelist() if common.CERT_PATH_REGEX.match(n)]
certs = [n for n in jar.namelist() if common.SIGNATURE_BLOCK_FILE_REGEX.match(n)]
if len(certs) < 1:
raise VerificationException(_("Found no signing certificates for repository."))
if len(certs) > 1:

View File

@ -280,7 +280,7 @@ def main():
signaturefile, signedfile, manifest = signingfiles
with open(signaturefile, 'rb') as f:
devfp = common.signer_fingerprint_short(f.read())
devfp = common.signer_fingerprint_short(common.get_signature(f.read()))
devsigned = '{}_{}_{}.apk'.format(appid, vercode, devfp)
devsignedtmp = os.path.join(tmp_dir, devsigned)
shutil.copy(apkfile, devsignedtmp)

View File

@ -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.CERT_PATH_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

View File

@ -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,

Binary file not shown.

View File

@ -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:

BIN
tests/v2.only.sig_2.apk Normal file

Binary file not shown.