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

Merge branch 'sign-and-verify-update' into 'master'

sign and verify update

See merge request !230
This commit is contained in:
Hans-Christoph Steiner 2017-03-27 19:48:36 +00:00
commit 7f08fad2c6
3 changed files with 136 additions and 32 deletions

View File

@ -2012,36 +2012,77 @@ def verify_apks(signed_apk, unsigned_apk, tmp_dir):
One of the inputs is signed, the other is unsigned. The signature metadata One of the inputs is signed, the other is unsigned. The signature metadata
is transferred from the signed to the unsigned apk, and then jarsigner is is transferred from the signed to the unsigned apk, and then jarsigner is
used to verify that the signature from the signed apk is also varlid for used to verify that the signature from the signed apk is also varlid for
the unsigned one. the unsigned one. If the APK given as unsigned actually does have a
signature, it will be stripped out and ignored.
There are two SHA1 git commit IDs that fdroidserver includes in the builds
it makes: fdroidserverid and buildserverid. Originally, these were inserted
into AndroidManifest.xml, but that makes the build not reproducible. So
instead they are included as separate files in the APK's META-INF/ folder.
If those files exist in the signed APK, they will be part of the signature
and need to also be included in the unsigned APK for it to validate.
:param signed_apk: Path to a signed apk file :param signed_apk: Path to a signed apk file
:param unsigned_apk: Path to an unsigned apk file expected to match it :param unsigned_apk: Path to an unsigned apk file expected to match it
:param tmp_dir: Path to directory for temporary files :param tmp_dir: Path to directory for temporary files
:returns: None if the verification is successful, otherwise a string :returns: None if the verification is successful, otherwise a string
describing what went wrong. describing what went wrong.
""" """
with ZipFile(signed_apk) as signed_apk_as_zip:
meta_inf_files = ['META-INF/MANIFEST.MF']
for f in signed_apk_as_zip.namelist():
if apk_sigfile.match(f):
meta_inf_files.append(f)
if len(meta_inf_files) < 3:
return "Signature files missing from {0}".format(signed_apk)
signed_apk_as_zip.extractall(tmp_dir, meta_inf_files)
with ZipFile(unsigned_apk, mode='a') as unsigned_apk_as_zip:
for meta_inf_file in meta_inf_files:
unsigned_apk_as_zip.write(os.path.join(tmp_dir, meta_inf_file), arcname=meta_inf_file)
if subprocess.call([config['jarsigner'], '-verify', unsigned_apk]) != 0: signed = ZipFile(signed_apk, 'r')
logging.info("...NOT verified - {0}".format(signed_apk)) meta_inf_files = ['META-INF/MANIFEST.MF']
return compare_apks(signed_apk, unsigned_apk, tmp_dir) for f in signed.namelist():
if apk_sigfile.match(f) \
or f in ['META-INF/fdroidserverid', 'META-INF/buildserverid']:
meta_inf_files.append(f)
if len(meta_inf_files) < 3:
return "Signature files missing from {0}".format(signed_apk)
tmp_apk = os.path.join(tmp_dir, 'sigcp_' + os.path.basename(unsigned_apk))
unsigned = ZipFile(unsigned_apk, 'r')
# only read the signature from the signed APK, everything else from unsigned
with ZipFile(tmp_apk, 'w') as tmp:
for filename in meta_inf_files:
tmp.writestr(signed.getinfo(filename), signed.read(filename))
for info in unsigned.infolist():
if info.filename in meta_inf_files:
logging.warning('Ignoring ' + info.filename + ' from ' + unsigned_apk)
continue
if info.filename in tmp.namelist():
return "duplicate filename found: " + info.filename
tmp.writestr(info, unsigned.read(info.filename))
unsigned.close()
signed.close()
verified = verify_apk_signature(tmp_apk)
if not verified:
logging.info("...NOT verified - {0}".format(tmp_apk))
return compare_apks(signed_apk, tmp_apk, tmp_dir, os.path.dirname(unsigned_apk))
logging.info("...successfully verified") logging.info("...successfully verified")
return None return None
def verify_apk_signature(apk):
"""verify the signature on an APK
Try to use apksigner whenever possible since jarsigner is very
shitty: unsigned APKs pass as "verified"! So this has to turn on
-strict then check for result 4.
"""
if set_command_in_config('apksigner'):
return subprocess.call([config['apksigner'], 'verify', apk]) == 0
else:
logging.warning("Using Java's jarsigner, not recommended for verifying APKs! Use apksigner")
return subprocess.call([config['jarsigner'], '-strict', '-verify', apk]) == 4
apk_badchars = re.compile('''[/ :;'"]''') apk_badchars = re.compile('''[/ :;'"]''')
def compare_apks(apk1, apk2, tmp_dir): def compare_apks(apk1, apk2, tmp_dir, log_dir=None):
"""Compare two apks """Compare two apks
Returns None if the apk content is the same (apart from the signing key), Returns None if the apk content is the same (apart from the signing key),
@ -2049,17 +2090,16 @@ def compare_apks(apk1, apk2, tmp_dir):
trying to do the comparison. trying to do the comparison.
""" """
if not log_dir:
log_dir = tmp_dir
absapk1 = os.path.abspath(apk1) absapk1 = os.path.abspath(apk1)
absapk2 = os.path.abspath(apk2) absapk2 = os.path.abspath(apk2)
# try to find diffoscope in the path, if it hasn't been manually configed if set_command_in_config('diffoscope'):
if 'diffoscope' not in config: logfilename = os.path.join(log_dir, os.path.basename(absapk1))
tmp = find_command('diffoscope') htmlfile = logfilename + '.diffoscope.html'
if tmp is not None: textfile = logfilename + '.diffoscope.txt'
config['diffoscope'] = tmp
if 'diffoscope' in config:
htmlfile = absapk1 + '.diffoscope.html'
textfile = absapk1 + '.diffoscope.txt'
if subprocess.call([config['diffoscope'], if subprocess.call([config['diffoscope'],
'--max-report-size', '12345678', '--max-diff-block-lines', '100', '--max-report-size', '12345678', '--max-diff-block-lines', '100',
'--html', htmlfile, '--text', textfile, '--html', htmlfile, '--text', textfile,
@ -2083,12 +2123,7 @@ def compare_apks(apk1, apk2, tmp_dir):
cwd=os.path.join(apk2dir, 'jar-xf')) != 0: cwd=os.path.join(apk2dir, 'jar-xf')) != 0:
return("Failed to unpack " + apk2) return("Failed to unpack " + apk2)
# try to find apktool in the path, if it hasn't been manually configed if set_command_in_config('apktool'):
if 'apktool' not in config:
tmp = find_command('apktool')
if tmp is not None:
config['apktool'] = tmp
if 'apktool' in config:
if subprocess.call([config['apktool'], 'd', os.path.abspath(apk1), '--output', 'apktool'], if subprocess.call([config['apktool'], 'd', os.path.abspath(apk1), '--output', 'apktool'],
cwd=apk1dir) != 0: cwd=apk1dir) != 0:
return("Failed to unpack " + apk1) return("Failed to unpack " + apk1)
@ -2112,6 +2147,22 @@ def compare_apks(apk1, apk2, tmp_dir):
return None return None
def set_command_in_config(command):
'''Try to find specified command in the path, if it hasn't been
manually set in config.py. If found, it is added to the config
dict. The return value says whether the command is available.
'''
if command in config:
return True
else:
tmp = find_command(command)
if tmp is not None:
config[command] = tmp
return True
return False
def find_command(command): def find_command(command):
'''find the full path of a command, or None if it can't be found in the PATH''' '''find the full path of a command, or None if it can't be found in the PATH'''

View File

@ -78,9 +78,9 @@ def main():
logging.info("...retrieving " + url) logging.info("...retrieving " + url)
net.download_file(url, dldir=tmp_dir) net.download_file(url, dldir=tmp_dir)
compare_result = common.compare_apks( compare_result = common.verify_apks(
os.path.join(unsigned_dir, apkfilename),
remoteapk, remoteapk,
os.path.join(unsigned_dir, apkfilename),
tmp_dir) tmp_dir)
if compare_result: if compare_result:
raise FDroidException(compare_result) raise FDroidException(compare_result)

View File

@ -10,6 +10,7 @@ import shutil
import sys import sys
import tempfile import tempfile
import unittest import unittest
from zipfile import ZipFile
localmodule = os.path.realpath( localmodule = os.path.realpath(
os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..')) os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..'))
@ -177,6 +178,58 @@ class CommonTest(unittest.TestCase):
# these should be resigned, and therefore different # these should be resigned, and therefore different
self.assertNotEqual(open(sourcefile, 'rb').read(), open(testfile, 'rb').read()) self.assertNotEqual(open(sourcefile, 'rb').read(), open(testfile, 'rb').read())
def test_verify_apk_signature(self):
fdroidserver.common.config = None
config = fdroidserver.common.read_config(fdroidserver.common.options)
config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
fdroidserver.common.config = config
self.assertTrue(fdroidserver.common.verify_apk_signature('urzip.apk'))
self.assertFalse(fdroidserver.common.verify_apk_signature('urzip-badcert.apk'))
self.assertFalse(fdroidserver.common.verify_apk_signature('urzip-badsig.apk'))
self.assertTrue(fdroidserver.common.verify_apk_signature('urzip-release.apk'))
self.assertFalse(fdroidserver.common.verify_apk_signature('urzip-release-unsigned.apk'))
def test_verify_apks(self):
fdroidserver.common.config = None
config = fdroidserver.common.read_config(fdroidserver.common.options)
config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
fdroidserver.common.config = config
basedir = os.path.dirname(__file__)
sourceapk = os.path.join(basedir, 'urzip.apk')
tmpdir = os.path.join(basedir, '..', '.testfiles')
if not os.path.exists(tmpdir):
os.makedirs(tmpdir)
testdir = tempfile.mkdtemp(prefix='test_verify_apks', dir=tmpdir)
print('testdir', testdir)
copyapk = os.path.join(testdir, 'urzip-copy.apk')
shutil.copy(sourceapk, copyapk)
self.assertTrue(fdroidserver.common.verify_apk_signature(copyapk))
self.assertIsNone(fdroidserver.common.verify_apks(sourceapk, copyapk, tmpdir))
unsignedapk = os.path.join(testdir, 'urzip-unsigned.apk')
with ZipFile(sourceapk, 'r') as apk:
with ZipFile(unsignedapk, 'w') as testapk:
for info in apk.infolist():
if not info.filename.startswith('META-INF/'):
testapk.writestr(info, apk.read(info.filename))
self.assertIsNone(fdroidserver.common.verify_apks(sourceapk, unsignedapk, tmpdir))
twosigapk = os.path.join(testdir, 'urzip-twosig.apk')
otherapk = ZipFile(os.path.join(basedir, 'urzip-release.apk'), 'r')
with ZipFile(sourceapk, 'r') as apk:
with ZipFile(twosigapk, 'w') as testapk:
for info in apk.infolist():
testapk.writestr(info, apk.read(info.filename))
if info.filename.startswith('META-INF/'):
testapk.writestr(info, otherapk.read(info.filename))
otherapk.close()
self.assertFalse(fdroidserver.common.verify_apk_signature(twosigapk))
self.assertIsNone(fdroidserver.common.verify_apks(sourceapk, twosigapk, tmpdir))
if __name__ == "__main__": if __name__ == "__main__":
parser = optparse.OptionParser() parser = optparse.OptionParser()