mirror of
				https://github.com/f-droid/fdroidserver.git
				synced 2025-11-04 06:30:27 +03: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:
		
						commit
						64bab7a94c
					
				
					 9 changed files with 124 additions and 52 deletions
				
			
		| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										
											BIN
										
									
								
								tests/repo/v1.v2.sig_1020.apk
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tests/repo/v1.v2.sig_1020.apk
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -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
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tests/v2.only.sig_2.apk
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue