mirror of
https://gitlab.com/fdroid/fdroidserver.git
synced 2024-11-14 19:10:11 +01:00
Merge branch 'sign-and-verify-update' into 'master'
sign and verify update See merge request !230
This commit is contained in:
commit
7f08fad2c6
@ -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'''
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user