mirror of
				https://github.com/f-droid/fdroidserver.git
				synced 2025-11-04 06:30:27 +03:00 
			
		
		
		
	added signatures subcommand
This commit is contained in:
		
							parent
							
								
									be874b1134
								
							
						
					
					
						commit
						3e6dfacf6c
					
				
					 5 changed files with 238 additions and 1 deletions
				
			
		
							
								
								
									
										1
									
								
								fdroid
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								fdroid
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -44,6 +44,7 @@ commands = OrderedDict([
 | 
			
		|||
    ("server", "Interact with the repo HTTP server"),
 | 
			
		||||
    ("signindex", "Sign indexes created using update --nosign"),
 | 
			
		||||
    ("btlog", "Update the binary transparency log for a URL"),
 | 
			
		||||
    ("signatures", "Extract signatures from APKs"),
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -260,7 +260,7 @@ def read_config(opts, config_file='config.py'):
 | 
			
		|||
    if any(k in config for k in ["keystore", "keystorepass", "keypass"]):
 | 
			
		||||
        st = os.stat(config_file)
 | 
			
		||||
        if st.st_mode & stat.S_IRWXG or st.st_mode & stat.S_IRWXO:
 | 
			
		||||
            logging.warn("unsafe permissions on {0} (should be 0600)!".format(config_file))
 | 
			
		||||
            logging.warning("unsafe permissions on {0} (should be 0600)!".format(config_file))
 | 
			
		||||
 | 
			
		||||
    fill_config_defaults(config)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1706,6 +1706,21 @@ def isApkAndDebuggable(apkfile):
 | 
			
		|||
        return get_apk_debuggable_androguard(apkfile)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_apk_id_aapt(apkfile):
 | 
			
		||||
    """Extrat identification information from APK using aapt.
 | 
			
		||||
 | 
			
		||||
    :param apkfile: path to an APK file.
 | 
			
		||||
    :returns: triplet (appid, version code, version name)
 | 
			
		||||
    """
 | 
			
		||||
    r = re.compile("package: name='(?P<appid>.*)' versionCode='(?P<vercode>.*)' versionName='(?P<vername>.*)' platformBuildVersionName='.*'")
 | 
			
		||||
    p = SdkToolsPopen(['aapt', 'dump', 'badging', apkfile], output=False)
 | 
			
		||||
    for line in p.output.splitlines():
 | 
			
		||||
        m = r.match(line)
 | 
			
		||||
        if m:
 | 
			
		||||
            return m.group('appid'), m.group('vercode'), m.group('vername')
 | 
			
		||||
    raise FDroidException("reading identification failed, APK invalid: '{}'".format(apkfile))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PopenResult:
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.returncode = None
 | 
			
		||||
| 
						 | 
				
			
			@ -1964,6 +1979,30 @@ def place_srclib(root_dir, number, libpath):
 | 
			
		|||
apk_sigfile = re.compile(r'META-INF/[0-9A-Za-z]+\.(SF|RSA|DSA|EC)')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def metadata_get_sigdir(appid, vercode=None):
 | 
			
		||||
    """Get signature directory for app"""
 | 
			
		||||
    if vercode:
 | 
			
		||||
        return os.path.join('metadata', appid, 'signatures', vercode)
 | 
			
		||||
    else:
 | 
			
		||||
        return os.path.join('metadata', appid, 'signatures')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def apk_extract_signatures(apkpath, outdir, manifest=True):
 | 
			
		||||
    """Extracts a signature files from APK and puts them into target directory.
 | 
			
		||||
 | 
			
		||||
    :param apkpath: location of the apk
 | 
			
		||||
    :param outdir: folder where the extracted signature files will be stored
 | 
			
		||||
    :param manifest: (optionally) disable extracting manifest file
 | 
			
		||||
    """
 | 
			
		||||
    with ZipFile(apkpath, 'r') as in_apk:
 | 
			
		||||
        for f in in_apk.infolist():
 | 
			
		||||
            if apk_sigfile.match(f.filename) or \
 | 
			
		||||
                    (manifest and f.filename == 'META-INF/MANIFEST.MF'):
 | 
			
		||||
                newpath = os.path.join(outdir, os.path.basename(f.filename))
 | 
			
		||||
                with open(newpath, 'wb') as out_file:
 | 
			
		||||
                    out_file.write(in_apk.read(f.filename))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def verify_apks(signed_apk, unsigned_apk, tmp_dir):
 | 
			
		||||
    """Verify that two apks are the same
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										98
									
								
								fdroidserver/signatures.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								fdroidserver/signatures.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,98 @@
 | 
			
		|||
#!/usr/bin/env python3
 | 
			
		||||
#
 | 
			
		||||
# Copyright (C) 2017, Michael Poehn <michael.poehn@fsfe.org>
 | 
			
		||||
#
 | 
			
		||||
# This program is free software: you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
# the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This program is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU Affero General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from . import common
 | 
			
		||||
from . import net
 | 
			
		||||
from .exception import FDroidException
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def extract_signature(apkpath):
 | 
			
		||||
 | 
			
		||||
    if not os.path.exists(apkpath):
 | 
			
		||||
        raise FDroidException("file APK does not exists '{}'".format(apkpath))
 | 
			
		||||
    if not common.verify_apk_signature(apkpath):
 | 
			
		||||
        raise FDroidException("no valid signature in '{}'".format(apkpath))
 | 
			
		||||
    logging.debug('signature okay: %s', apkpath)
 | 
			
		||||
 | 
			
		||||
    appid, vercode, _ = common.get_apk_id_aapt(apkpath)
 | 
			
		||||
    sigdir = common.metadata_get_sigdir(appid, vercode)
 | 
			
		||||
    if not os.path.exists(sigdir):
 | 
			
		||||
        os.makedirs(sigdir)
 | 
			
		||||
    common.apk_extract_signatures(apkpath, sigdir)
 | 
			
		||||
 | 
			
		||||
    return sigdir
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def extract(config, options):
 | 
			
		||||
 | 
			
		||||
    # Create tmp dir if missing...
 | 
			
		||||
    tmp_dir = 'tmp'
 | 
			
		||||
    if not os.path.exists(tmp_dir):
 | 
			
		||||
        os.mkdir(tmp_dir)
 | 
			
		||||
 | 
			
		||||
    if not options.APK or len(options.APK) <= 0:
 | 
			
		||||
        logging.critical('no APK supplied')
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
    # iterate over supplied APKs downlaod and extract them...
 | 
			
		||||
    httpre = re.compile('https?:\/\/')
 | 
			
		||||
    for apk in options.APK:
 | 
			
		||||
        try:
 | 
			
		||||
            if os.path.isfile(apk):
 | 
			
		||||
                sigdir = extract_signature(apk)
 | 
			
		||||
                logging.info('fetched singatures for %s -> %s', apk, sigdir)
 | 
			
		||||
            elif httpre.match(apk):
 | 
			
		||||
                if apk.startswith('https') or options.no_check_https:
 | 
			
		||||
                    try:
 | 
			
		||||
                        tmp_apk = os.path.join(tmp_dir, 'signed.apk')
 | 
			
		||||
                        net.download_file(apk, tmp_apk)
 | 
			
		||||
                        sigdir = extract_signature(tmp_apk)
 | 
			
		||||
                        logging.info('fetched singatures for %s -> %s', apk, sigdir)
 | 
			
		||||
                    finally:
 | 
			
		||||
                        if tmp_apk and os.path.exists(tmp_apk):
 | 
			
		||||
                            os.remove(tmp_apk)
 | 
			
		||||
                else:
 | 
			
		||||
                    logging.warn('refuse downloading via insecure http connection (use https or specify --no-https-check): %s', apk)
 | 
			
		||||
        except FDroidException as e:
 | 
			
		||||
            logging.warning("failed fetching signatures for '%s': %s", apk, e)
 | 
			
		||||
            if e.detail:
 | 
			
		||||
                logging.debug(e.detail)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
 | 
			
		||||
    global config, options
 | 
			
		||||
 | 
			
		||||
    # Parse command line...
 | 
			
		||||
    parser = ArgumentParser(usage="%(prog)s [options] APK [APK...]")
 | 
			
		||||
    common.setup_global_opts(parser)
 | 
			
		||||
    parser.add_argument("APK", nargs='*',
 | 
			
		||||
                        help="signed APK, either a file-path or Https-URL are fine here.")
 | 
			
		||||
    parser.add_argument("--no-check-https", action="store_true", default=False)
 | 
			
		||||
    options = parser.parse_args()
 | 
			
		||||
 | 
			
		||||
    # Read config.py...
 | 
			
		||||
    config = common.read_config(options)
 | 
			
		||||
 | 
			
		||||
    extract(config, options)
 | 
			
		||||
							
								
								
									
										65
									
								
								tests/signatures.TestCase
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										65
									
								
								tests/signatures.TestCase
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,65 @@
 | 
			
		|||
#!/usr/bin/env python3
 | 
			
		||||
 | 
			
		||||
import inspect
 | 
			
		||||
import optparse
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
import unittest
 | 
			
		||||
import hashlib
 | 
			
		||||
import logging
 | 
			
		||||
from tempfile import TemporaryDirectory
 | 
			
		||||
 | 
			
		||||
localmodule = os.path.realpath(
 | 
			
		||||
    os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..'))
 | 
			
		||||
print('localmodule: ' + localmodule)
 | 
			
		||||
if localmodule not in sys.path:
 | 
			
		||||
    sys.path.insert(0, localmodule)
 | 
			
		||||
 | 
			
		||||
from testcommon import TmpCwd
 | 
			
		||||
from fdroidserver import common, signatures
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SignaturesTest(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        logging.basicConfig(level=logging.DEBUG)
 | 
			
		||||
        common.config = None
 | 
			
		||||
        config = common.read_config(common.options)
 | 
			
		||||
        config['jarsigner'] = common.find_sdk_tools_cmd('jarsigner')
 | 
			
		||||
        config['verbose'] = True
 | 
			
		||||
        common.config = config
 | 
			
		||||
 | 
			
		||||
    def test_main(self):
 | 
			
		||||
 | 
			
		||||
        # option fixture class:
 | 
			
		||||
        class OptionsFixture:
 | 
			
		||||
            APK = [os.path.abspath(os.path.join('repo', 'com.politedroid_3.apk'))]
 | 
			
		||||
 | 
			
		||||
        with TemporaryDirectory() as tmpdir, TmpCwd(tmpdir):
 | 
			
		||||
            signatures.extract(common.config, OptionsFixture())
 | 
			
		||||
 | 
			
		||||
            # check if extracted signatures are where they are supposed to be
 | 
			
		||||
            # also verify weather if extracted file contian what they should
 | 
			
		||||
            filesAndHashes = (
 | 
			
		||||
                (os.path.join('metadata', 'com.politedroid', 'signatures', '3', 'MANIFEST.MF'),
 | 
			
		||||
                 '7dcd83f0c41a75457fd2311bf3c4578f80d684362d74ba8dc52838d353f31cf2'),
 | 
			
		||||
                (os.path.join('metadata', 'com.politedroid', 'signatures', '3', 'RELEASE.RSA'),
 | 
			
		||||
                 '883ef3d5a6e0bf69d2a58d9e255a7930f08a49abc38e216ed054943c99c8fdb4'),
 | 
			
		||||
                (os.path.join('metadata', 'com.politedroid', 'signatures', '3', 'RELEASE.SF'),
 | 
			
		||||
                 '99fbb3211ef5d7c1253f3a7ad4836eadc9905103ce6a75916c40de2831958284'),
 | 
			
		||||
            )
 | 
			
		||||
            for path, checksum in filesAndHashes:
 | 
			
		||||
                self.assertTrue(os.path.isfile(path))
 | 
			
		||||
                with open(path, 'rb') as f:
 | 
			
		||||
                    self.assertEqual(hashlib.sha256(f.read()).hexdigest(), checksum)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    parser = optparse.OptionParser()
 | 
			
		||||
    parser.add_option("-v", "--verbose", action="store_true", default=False,
 | 
			
		||||
                      help="Spew out even more information than normal")
 | 
			
		||||
    (common.options, args) = parser.parse_args(['--verbose'])
 | 
			
		||||
 | 
			
		||||
    newSuite = unittest.TestSuite()
 | 
			
		||||
    newSuite.addTest(unittest.makeSuite(SignaturesTest))
 | 
			
		||||
    unittest.main()
 | 
			
		||||
							
								
								
									
										34
									
								
								tests/testcommon.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								tests/testcommon.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
#!/usr/bin/env python3
 | 
			
		||||
#
 | 
			
		||||
# Copyright (C) 2017, Michael Poehn <michael.poehn@fsfe.org>
 | 
			
		||||
#
 | 
			
		||||
# This program is free software: you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
# the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This program is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU Affero General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TmpCwd():
 | 
			
		||||
    """Context-manager for temporarily changing the current working
 | 
			
		||||
    directory.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, new_cwd):
 | 
			
		||||
        self.new_cwd = new_cwd
 | 
			
		||||
 | 
			
		||||
    def __enter__(self):
 | 
			
		||||
        self.orig_cwd = os.getcwd()
 | 
			
		||||
        os.chdir(self.new_cwd)
 | 
			
		||||
 | 
			
		||||
    def __exit__(self, a, b, c):
 | 
			
		||||
        os.chdir(self.orig_cwd)
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue