Merge branch 'signing' into 'master'

sign using apksigner

Closes #827 and #634

See merge request fdroid/fdroidserver!736
This commit is contained in:
Hans-Christoph Steiner 2020-09-10 11:49:02 +00:00
commit 06766ba48b
6 changed files with 160 additions and 110 deletions

View file

@ -71,7 +71,10 @@ FDROID_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))
# this is the build-tools version, aapt has a separate version that
# has to be manually set in test_aapt_version()
MINIMUM_AAPT_VERSION = '26.0.0'
MINIMUM_AAPT_BUILD_TOOLS_VERSION = '26.0.0'
# 26.0.2 is the first version recognizing md5 based signatures as valid again
# (as does android, so we want that)
MINIMUM_APKSIGNER_BUILD_TOOLS_VERSION = '26.0.2'
VERCODE_OPERATION_RE = re.compile(r'^([ 0-9/*+-]|%c)+$')
@ -111,7 +114,7 @@ default_config = {
'r16b': None,
},
'cachedir': os.path.join(os.getenv('HOME'), '.cache', 'fdroidserver'),
'build_tools': MINIMUM_AAPT_VERSION,
'build_tools': MINIMUM_AAPT_BUILD_TOOLS_VERSION,
'force_build_tools': False,
'java_paths': None,
'scan_binary': False,
@ -321,8 +324,7 @@ def read_config(opts, config_file='config.py'):
config['smartcardoptions'] = re.sub(r'\s+', r' ', config['smartcardoptions']).split(' ')
elif not smartcardoptions and 'keystore' in config and config['keystore'] == 'NONE':
# keystore='NONE' means use smartcard, these are required defaults
config['smartcardoptions'] = ['-storetype', 'PKCS11', '-providerName',
'SunPKCS11-OpenSC', '-providerClass',
config['smartcardoptions'] = ['-storetype', 'PKCS11', '-providerClass',
'sun.security.pkcs11.SunPKCS11',
'-providerArg', 'opensc-fdroid.cfg']
@ -416,6 +418,28 @@ def assert_config_keystore(config):
+ "you can create one using: fdroid update --create-key")
def find_apksigner():
"""
Returns the best version of apksigner following this algorithm:
* use config['apksigner'] if set
* try to find apksigner in path
* find apksigner in build-tools starting from newest installed going down to MINIMUM_APKSIGNER_BUILD_TOOLS_VERSION
:return: path to apksigner or None if no version is found
"""
if set_command_in_config('apksigner'):
return config['apksigner']
build_tools_path = os.path.join(config['sdk_path'], 'build-tools')
for f in sorted(os.listdir(build_tools_path), reverse=True):
if LooseVersion(f) < LooseVersion(MINIMUM_AAPT_BUILD_TOOLS_VERSION):
return None
if os.path.exists(os.path.join(build_tools_path, f, 'apksigner')):
apksigner = os.path.join(build_tools_path, f, 'apksigner')
logging.info("Using %s " % apksigner)
# memoize result
config['apksigner'] = apksigner
return config['apksigner']
def find_sdk_tools_cmd(cmd):
'''find a working path to a tool from the Android SDK'''
@ -466,13 +490,13 @@ def test_aapt_version(aapt):
# the Debian package has the version string like "v0.2-23.0.2"
too_old = False
if '.' in bugfix:
if LooseVersion(bugfix) < LooseVersion(MINIMUM_AAPT_VERSION):
if LooseVersion(bugfix) < LooseVersion(MINIMUM_AAPT_BUILD_TOOLS_VERSION):
too_old = True
elif LooseVersion('.'.join((major, minor, bugfix))) < LooseVersion('0.2.4062713'):
too_old = True
if too_old:
logging.warning(_("'{aapt}' is too old, fdroid requires build-tools-{version} or newer!")
.format(aapt=aapt, version=MINIMUM_AAPT_VERSION))
.format(aapt=aapt, version=MINIMUM_AAPT_BUILD_TOOLS_VERSION))
else:
logging.warning(_('Unknown version of aapt, might cause problems: ') + output)
@ -2333,7 +2357,7 @@ def _get_androguard_APK(apkfile):
try:
from androguard.core.bytecodes.apk import APK
except ImportError:
raise FDroidException("androguard library is not installed and aapt not present")
raise FDroidException("androguard library is not installed")
return APK(apkfile)
@ -2510,21 +2534,6 @@ def get_native_code(apkfile):
return sorted(list(archset))
def get_minSdkVersion(apkfile):
"""Extract the minimum supported Android SDK from an APK using androguard
:param apkfile: path to an APK file.
:returns: the integer representing the SDK version
"""
try:
apk = _get_androguard_APK(apkfile)
except FileNotFoundError:
raise FDroidException(_('Reading minSdkVersion failed: "{apkfilename}"')
.format(apkfilename=apkfile))
return int(apk.get_min_sdk_version())
class PopenResult:
def __init__(self):
self.returncode = None
@ -2954,7 +2963,7 @@ def metadata_find_developer_signing_files(appid, vercode):
return None
def apk_strip_signatures(signed_apk, strip_manifest=False):
def apk_strip_v1_signatures(signed_apk, strip_manifest=False):
"""Removes signatures from APK.
:param signed_apk: path to apk file.
@ -3037,36 +3046,76 @@ def apk_extract_signatures(apkpath, outdir, manifest=True):
def sign_apk(unsigned_path, signed_path, keyalias):
"""Sign and zipalign an unsigned APK, then save to a new file, deleting the unsigned
android-18 (4.3) finally added support for reasonable hash
algorithms, like SHA-256, before then, the only options were MD5
and SHA1 :-/ This aims to use SHA-256 when the APK does not target
Use apksigner for making v2 and v3 signature for apks with targetSDK >=30 as
otherwise they won't be installable on Android 11/R.
Otherwise use jarsigner for v1 only signatures until we have apksig v2/v3
signature transplantig support.
When using jarsigner we need to manually select the hash algorithm,
apksigner does this automatically. Apksigner also does the zipalign for us.
SHA-256 support was added in android-18 (4.3), before then, the only options were MD5
and SHA1. This aims to use SHA-256 when the APK does not target
older Android versions, and is therefore safe to do so.
https://issuetracker.google.com/issues/36956587
https://android-review.googlesource.com/c/platform/libcore/+/44491
"""
if get_minSdkVersion(unsigned_path) < 18:
signature_algorithm = ['-sigalg', 'SHA1withRSA', '-digestalg', 'SHA1']
apk = _get_androguard_APK(unsigned_path)
if int(apk.get_target_sdk_version()) >= 30:
if config['keystore'] == 'NONE':
# NOTE: apksigner doesn't like -providerName/--provider-name at all, don't use
# apksigner documents the options as --ks-provider-class and --ks-provider-arg
# those seem to be accepted but fail when actually making a signature with
# weird internal exceptions. Those options actually work.
# From: https://geoffreymetais.github.io/code/key-signing/#scripting
replacements = {'-storetype': '--ks-type',
'-providerClass': '--provider-class',
'-providerArg': '--provider-arg'}
signing_args = [replacements.get(n, n) for n in config['smartcardoptions']]
else:
signing_args = ['--key-pass', 'env:FDROID_KEY_PASS']
if not find_apksigner():
raise BuildException(_("apksigner not found, it's required for signing!"))
cmd = [find_apksigner(), 'sign',
'--ks', config['keystore'],
'--ks-pass', 'env:FDROID_KEY_STORE_PASS']
cmd += signing_args
cmd += ['--ks-key-alias', keyalias,
'--in', unsigned_path,
'--out', signed_path]
p = FDroidPopen(cmd, envs={
'FDROID_KEY_STORE_PASS': config['keystorepass'],
'FDROID_KEY_PASS': config.get('keypass', "")})
if p.returncode != 0:
raise BuildException(_("Failed to sign application"), p.output)
os.remove(unsigned_path)
else:
signature_algorithm = ['-sigalg', 'SHA256withRSA', '-digestalg', 'SHA-256']
cmd = [config['jarsigner'], '-keystore', config['keystore'],
'-storepass:env', 'FDROID_KEY_STORE_PASS']
if config['keystore'] == 'NONE':
cmd += config['smartcardoptions']
else:
cmd += '-keypass:env', 'FDROID_KEY_PASS'
p = FDroidPopen(cmd + signature_algorithm + [unsigned_path, keyalias],
envs={
'FDROID_KEY_STORE_PASS': config['keystorepass'],
'FDROID_KEY_PASS': config.get('keypass', "")})
if p.returncode != 0:
raise BuildException(_("Failed to sign application"), p.output)
if int(apk.get_min_sdk_version()) < 18:
signature_algorithm = ['-sigalg', 'SHA1withRSA', '-digestalg', 'SHA1']
else:
signature_algorithm = ['-sigalg', 'SHA256withRSA', '-digestalg', 'SHA-256']
if config['keystore'] == 'NONE':
signing_args = config['smartcardoptions']
else:
signing_args = ['-keypass:env', 'FDROID_KEY_PASS']
_zipalign(unsigned_path, signed_path)
os.remove(unsigned_path)
cmd = [config['jarsigner'], '-keystore', config['keystore'],
'-storepass:env', 'FDROID_KEY_STORE_PASS']
cmd += signing_args
cmd += signature_algorithm
cmd += [unsigned_path, keyalias]
p = FDroidPopen(cmd, envs={
'FDROID_KEY_STORE_PASS': config['keystorepass'],
'FDROID_KEY_PASS': config.get('keypass', "")})
if p.returncode != 0:
raise BuildException(_("Failed to sign application"), p.output)
_zipalign(unsigned_path, signed_path)
os.remove(unsigned_path)
def verify_apks(signed_apk, unsigned_apk, tmp_dir):

View file

@ -194,7 +194,7 @@ def main():
common.write_to_config(test_config, 'repo_keyalias', '1') # seems to be the default
disable_in_config('keypass', 'never used with smartcard')
common.write_to_config(test_config, 'smartcardoptions',
('-storetype PKCS11 -providerName SunPKCS11-OpenSC '
('-storetype PKCS11 '
+ '-providerClass sun.security.pkcs11.SunPKCS11 '
+ '-providerArg opensc-fdroid.cfg'))
# find opensc-pkcs11.so

View file

@ -268,7 +268,7 @@ Last updated: {date}'''.format(repo_git_base=repo_git_base,
os.chmod(apkfilename, 0o644)
logging.debug(_('Resigning {apkfilename} with provided debug.keystore')
.format(apkfilename=os.path.basename(apkfilename)))
common.apk_strip_signatures(apkfilename, strip_manifest=True)
common.apk_strip_v1_signatures(apkfilename, strip_manifest=True)
common.sign_apk(apkfilename, destapk, KEY_ALIAS)
if options.verbose: