mirror of
https://gitlab.com/fdroid/fdroidserver.git
synced 2024-10-06 11:00:13 +02:00
Merge branch '291-include-apk-signatures-in-build-metadata-file' into 'master'
include APK signatures in build metadata file Closes #291 See merge request fdroid/fdroidserver!287
This commit is contained in:
commit
ad10bfcad2
@ -34,6 +34,9 @@ import logging
|
||||
import hashlib
|
||||
import socket
|
||||
import base64
|
||||
import zipfile
|
||||
import tempfile
|
||||
import json
|
||||
import xml.etree.ElementTree as XMLElementTree
|
||||
|
||||
from binascii import hexlify
|
||||
@ -509,6 +512,32 @@ def publishednameinfo(filename):
|
||||
return result
|
||||
|
||||
|
||||
apk_release_filename = re.compile('(?P<appid>[a-zA-Z0-9_\.]+)_(?P<vercode>[0-9]+)\.apk')
|
||||
apk_release_filename_with_sigfp = re.compile('(?P<appid>[a-zA-Z0-9_\.]+)_(?P<vercode>[0-9]+)_(?P<sigfp>[0-9a-f]{7})\.apk')
|
||||
|
||||
|
||||
def apk_parse_release_filename(apkname):
|
||||
"""Parses the name of an APK file according the F-Droids APK naming
|
||||
scheme and returns the tokens.
|
||||
|
||||
WARNING: Returned values don't necessarily represent the APKs actual
|
||||
properties, the are just paresed from the file name.
|
||||
|
||||
:returns: A triplet containing (appid, versionCode, signer), where appid
|
||||
should be the package name, versionCode should be the integer
|
||||
represion of the APKs version and signer should be the first 7 hex
|
||||
digists of the sha256 signing key fingerprint which was used to sign
|
||||
this APK.
|
||||
"""
|
||||
m = apk_release_filename_with_sigfp.match(apkname)
|
||||
if m:
|
||||
return m.group('appid'), m.group('vercode'), m.group('sigfp')
|
||||
m = apk_release_filename.match(apkname)
|
||||
if m:
|
||||
return m.group('appid'), m.group('vercode'), None
|
||||
return None, None, None
|
||||
|
||||
|
||||
def get_release_filename(app, build):
|
||||
if build.output:
|
||||
return "%s_%s.%s" % (app.id, build.versionCode, get_file_extension(build.output))
|
||||
@ -2014,6 +2043,67 @@ def place_srclib(root_dir, number, libpath):
|
||||
apk_sigfile = 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.
|
||||
|
||||
Extracts the first 7 hexadecimal digits of sha256 signing-key fingerprint
|
||||
for a given pkcs7 signature.
|
||||
|
||||
:param sig: Contents of an APK signing certificate.
|
||||
:returns: shortened signing-key fingerprint.
|
||||
"""
|
||||
return signer_fingerprint(sig)[:7]
|
||||
|
||||
|
||||
def signer_fingerprint(sig):
|
||||
"""Obtain sha256 signing-key fingerprint for pkcs7 signature.
|
||||
|
||||
Extracts hexadecimal sha256 signing-key fingerprint string
|
||||
for a given pkcs7 signature.
|
||||
|
||||
:param: Contents of an APK signature.
|
||||
:returns: shortened signature fingerprint.
|
||||
"""
|
||||
cert_encoded = get_certificate(sig)
|
||||
return hashlib.sha256(cert_encoded).hexdigest()
|
||||
|
||||
|
||||
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
|
||||
: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)
|
||||
|
||||
|
||||
def apk_signer_fingerprint_short(apk_path):
|
||||
"""Obtain shortened sha256 signing-key fingerprint for APK.
|
||||
|
||||
Extracts the first 7 hexadecimal digits of sha256 signing-key fingerprint
|
||||
for a given pkcs7 APK.
|
||||
|
||||
:param apk_path: path to APK
|
||||
:returns: shortened signing-key fingerprint
|
||||
"""
|
||||
return apk_signer_fingerprint(apk_path)[:7]
|
||||
|
||||
|
||||
def metadata_get_sigdir(appid, vercode=None):
|
||||
"""Get signature directory for app"""
|
||||
if vercode:
|
||||
@ -2022,6 +2112,128 @@ def metadata_get_sigdir(appid, vercode=None):
|
||||
return os.path.join('metadata', appid, 'signatures')
|
||||
|
||||
|
||||
def metadata_find_developer_signature(appid, vercode=None):
|
||||
"""Tires to find the developer signature for given appid.
|
||||
|
||||
This picks the first signature file found in metadata an returns its
|
||||
signature.
|
||||
|
||||
:returns: sha256 signing key fingerprint of the developer signing key.
|
||||
None in case no signature can not be found."""
|
||||
|
||||
# fetch list of dirs for all versions of signatures
|
||||
appversigdirs = []
|
||||
if vercode:
|
||||
appversigdirs.append(metadata_get_sigdir(appid, vercode))
|
||||
else:
|
||||
appsigdir = metadata_get_sigdir(appid)
|
||||
if os.path.isdir(appsigdir):
|
||||
numre = re.compile('[0-9]+')
|
||||
for ver in os.listdir(appsigdir):
|
||||
if numre.match(ver):
|
||||
appversigdir = os.path.join(appsigdir, ver)
|
||||
appversigdirs.append(appversigdir)
|
||||
|
||||
for sigdir in appversigdirs:
|
||||
sigs = glob.glob(os.path.join(sigdir, '*.DSA')) + \
|
||||
glob.glob(os.path.join(sigdir, '*.EC')) + \
|
||||
glob.glob(os.path.join(sigdir, '*.RSA'))
|
||||
if len(sigs) > 1:
|
||||
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 None
|
||||
|
||||
|
||||
def metadata_find_signing_files(appid, vercode):
|
||||
"""Gets a list of singed manifests and signatures.
|
||||
|
||||
:param appid: app id string
|
||||
:param vercode: app version code
|
||||
:returns: a list of triplets for each signing key with following paths:
|
||||
(signature_file, singed_file, manifest_file)
|
||||
"""
|
||||
ret = []
|
||||
sigdir = metadata_get_sigdir(appid, vercode)
|
||||
sigs = glob.glob(os.path.join(sigdir, '*.DSA')) + \
|
||||
glob.glob(os.path.join(sigdir, '*.EC')) + \
|
||||
glob.glob(os.path.join(sigdir, '*.RSA'))
|
||||
extre = re.compile('(\.DSA|\.EC|\.RSA)$')
|
||||
for sig in sigs:
|
||||
sf = extre.sub('.SF', sig)
|
||||
if os.path.isfile(sf):
|
||||
mf = os.path.join(sigdir, 'MANIFEST.MF')
|
||||
if os.path.isfile(mf):
|
||||
ret.append((sig, sf, mf))
|
||||
return ret
|
||||
|
||||
|
||||
def metadata_find_developer_signing_files(appid, vercode):
|
||||
"""Get developer signature files for specified app from metadata.
|
||||
|
||||
:returns: A triplet of paths for signing files from metadata:
|
||||
(signature_file, singed_file, manifest_file)
|
||||
"""
|
||||
allsigningfiles = metadata_find_signing_files(appid, vercode)
|
||||
if allsigningfiles and len(allsigningfiles) == 1:
|
||||
return allsigningfiles[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def apk_strip_signatures(signed_apk, strip_manifest=False):
|
||||
"""Removes signatures from APK.
|
||||
|
||||
:param signed_apk: path to apk file.
|
||||
:param strip_manifest: when set to True also the manifest file will
|
||||
be removed from the APK.
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmp_apk = os.path.join(tmpdir, 'tmp.apk')
|
||||
os.rename(signed_apk, tmp_apk)
|
||||
with ZipFile(tmp_apk, 'r') as in_apk:
|
||||
with ZipFile(signed_apk, 'w') as out_apk:
|
||||
for f in in_apk.infolist():
|
||||
if not apk_sigfile.match(f.filename):
|
||||
if strip_manifest:
|
||||
if f.filename != 'META-INF/MANIFEST.MF':
|
||||
buf = in_apk.read(f.filename)
|
||||
out_apk.writestr(f.filename, buf)
|
||||
else:
|
||||
buf = in_apk.read(f.filename)
|
||||
out_apk.writestr(f.filename, buf)
|
||||
|
||||
|
||||
def apk_implant_signatures(apkpath, signaturefile, signedfile, manifest):
|
||||
"""Implats a signature from metadata into an APK.
|
||||
|
||||
Note: this changes there supplied APK in place. So copy it if you
|
||||
need the original to be preserved.
|
||||
|
||||
:param apkpath: location of the apk
|
||||
"""
|
||||
# get list of available signature files in metadata
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# orig_apk = os.path.join(tmpdir, 'orig.apk')
|
||||
# os.rename(apkpath, orig_apk)
|
||||
apkwithnewsig = os.path.join(tmpdir, 'newsig.apk')
|
||||
with ZipFile(apkpath, 'r') as in_apk:
|
||||
with ZipFile(apkwithnewsig, 'w') as out_apk:
|
||||
for sig_file in [signaturefile, signedfile, manifest]:
|
||||
out_apk.write(sig_file, arcname='META-INF/' +
|
||||
os.path.basename(sig_file))
|
||||
for f in in_apk.infolist():
|
||||
if not apk_sigfile.match(f.filename):
|
||||
if f.filename != 'META-INF/MANIFEST.MF':
|
||||
buf = in_apk.read(f.filename)
|
||||
out_apk.writestr(f.filename, buf)
|
||||
os.remove(apkpath)
|
||||
p = SdkToolsPopen(['zipalign', '-v', '4', apkwithnewsig, apkpath])
|
||||
if p.returncode != 0:
|
||||
raise BuildException("Failed to align application")
|
||||
|
||||
|
||||
def apk_extract_signatures(apkpath, outdir, manifest=True):
|
||||
"""Extracts a signature files from APK and puts them into target directory.
|
||||
|
||||
@ -2061,7 +2273,13 @@ def verify_apks(signed_apk, unsigned_apk, tmp_dir):
|
||||
describing what went wrong.
|
||||
"""
|
||||
|
||||
signed = ZipFile(signed_apk, 'r')
|
||||
if not os.path.isfile(signed_apk):
|
||||
return 'can not verify: file does not exists: {}'.format(signed_apk)
|
||||
|
||||
if not os.path.isfile(unsigned_apk):
|
||||
return 'can not verify: file does not exists: {}'.format(unsigned_apk)
|
||||
|
||||
with ZipFile(signed_apk, 'r') as signed:
|
||||
meta_inf_files = ['META-INF/MANIFEST.MF']
|
||||
for f in signed.namelist():
|
||||
if apk_sigfile.match(f) \
|
||||
@ -2071,20 +2289,19 @@ def verify_apks(signed_apk, unsigned_apk, tmp_dir):
|
||||
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')
|
||||
with ZipFile(unsigned_apk, 'r') as unsigned:
|
||||
# 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)
|
||||
logging.warning('Ignoring %s from %s',
|
||||
info.filename, 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)
|
||||
|
||||
@ -2348,6 +2565,34 @@ def get_certificate(certificate_file):
|
||||
return encoder.encode(cert)
|
||||
|
||||
|
||||
def load_stats_fdroid_signing_key_fingerprints():
|
||||
"""Load list of signing-key fingerprints stored by fdroid publish from file.
|
||||
|
||||
:returns: list of dictionanryies containing the singing-key fingerprints.
|
||||
"""
|
||||
jar_file = os.path.join('stats', 'publishsigkeys.jar')
|
||||
if not os.path.isfile(jar_file):
|
||||
return {}
|
||||
cmd = [config['jarsigner'], '-strict', '-verify', jar_file]
|
||||
p = FDroidPopen(cmd, output=False)
|
||||
if p.returncode != 4:
|
||||
raise FDroidException("Signature validation of '{}' failed! "
|
||||
"Please run publish again to rebuild this file.".format(jar_file))
|
||||
|
||||
jar_sigkey = apk_signer_fingerprint(jar_file)
|
||||
repo_key_sig = config.get('repo_key_sha256')
|
||||
if repo_key_sig:
|
||||
if jar_sigkey != repo_key_sig:
|
||||
raise FDroidException("Signature key fingerprint of file '{}' does not match repo_key_sha256 in config.py (found fingerprint: '{}')".format(jar_file, jar_sigkey))
|
||||
else:
|
||||
logging.warning("repo_key_sha256 not in config.py, setting it to the signature key fingerprint of '{}'".format(jar_file))
|
||||
config['repo_key_sha256'] = jar_sigkey
|
||||
write_to_config(config, 'repo_key_sha256')
|
||||
|
||||
with zipfile.ZipFile(jar_file, 'r') as f:
|
||||
return json.loads(str(f.read('publishsigkeys.json'), 'utf-8'))
|
||||
|
||||
|
||||
def write_to_config(thisconfig, key, value=None, config_file=None):
|
||||
'''write a key/value to the local config.py
|
||||
|
||||
|
@ -39,7 +39,7 @@ from . import common
|
||||
from . import metadata
|
||||
from . import net
|
||||
from . import signindex
|
||||
from fdroidserver.common import FDroidPopen, FDroidPopenBytes
|
||||
from fdroidserver.common import FDroidPopen, FDroidPopenBytes, load_stats_fdroid_signing_key_fingerprints
|
||||
from fdroidserver.exception import FDroidException, VerificationException, MetaDataException
|
||||
|
||||
|
||||
@ -151,11 +151,15 @@ def make(apps, sortedids, apks, repodir, archive):
|
||||
raise TypeError(_('only accepts strings, lists, and tuples'))
|
||||
requestsdict[command] = packageNames
|
||||
|
||||
make_v0(appsWithPackages, apks, repodir, repodict, requestsdict)
|
||||
make_v1(appsWithPackages, apks, repodir, repodict, requestsdict)
|
||||
fdroid_signing_key_fingerprints = load_stats_fdroid_signing_key_fingerprints()
|
||||
|
||||
make_v0(appsWithPackages, apks, repodir, repodict, requestsdict,
|
||||
fdroid_signing_key_fingerprints)
|
||||
make_v1(appsWithPackages, apks, repodir, repodict, requestsdict,
|
||||
fdroid_signing_key_fingerprints)
|
||||
|
||||
|
||||
def make_v1(apps, packages, repodir, repodict, requestsdict):
|
||||
def make_v1(apps, packages, repodir, repodict, requestsdict, fdroid_signing_key_fingerprints):
|
||||
|
||||
def _index_encoder_default(obj):
|
||||
if isinstance(obj, set):
|
||||
@ -168,6 +172,9 @@ def make_v1(apps, packages, repodir, repodict, requestsdict):
|
||||
output['repo'] = repodict
|
||||
output['requests'] = requestsdict
|
||||
|
||||
# establish sort order of the index
|
||||
v1_sort_packages(packages, repodir, fdroid_signing_key_fingerprints)
|
||||
|
||||
appslist = []
|
||||
output['apps'] = appslist
|
||||
for packageName, appdict in apps.items():
|
||||
@ -234,7 +241,43 @@ def make_v1(apps, packages, repodir, repodict, requestsdict):
|
||||
signindex.sign_index_v1(repodir, json_name)
|
||||
|
||||
|
||||
def make_v0(apps, apks, repodir, repodict, requestsdict):
|
||||
def v1_sort_packages(packages, repodir, fdroid_signing_key_fingerprints):
|
||||
"""Sorts the supplied list to ensure a deterministic sort order for
|
||||
package entries in the index file. This sort-order also expresses
|
||||
installation preference to the clients.
|
||||
(First in this list = first to install)
|
||||
|
||||
:param packages: list of packages which need to be sorted before but into index file.
|
||||
"""
|
||||
|
||||
GROUP_DEV_SIGNED = 1
|
||||
GROUP_FDROID_SIGNED = 2
|
||||
GROUP_OTHER_SIGNED = 3
|
||||
|
||||
def v1_sort_keys(package):
|
||||
packageName = package.get('packageName', None)
|
||||
|
||||
sig = package.get('signer', None)
|
||||
|
||||
dev_sig = common.metadata_find_developer_signature(packageName)
|
||||
group = GROUP_OTHER_SIGNED
|
||||
if dev_sig and dev_sig == sig:
|
||||
group = GROUP_DEV_SIGNED
|
||||
else:
|
||||
fdroidsig = fdroid_signing_key_fingerprints.get(packageName, {}).get('signer')
|
||||
if fdroidsig and fdroidsig == sig:
|
||||
group = GROUP_FDROID_SIGNED
|
||||
|
||||
versionCode = None
|
||||
if package.get('versionCode', None):
|
||||
versionCode = -int(package['versionCode'])
|
||||
|
||||
return(packageName, group, sig, versionCode)
|
||||
|
||||
packages.sort(key=v1_sort_keys)
|
||||
|
||||
|
||||
def make_v0(apps, apks, repodir, repodict, requestsdict, fdroid_signing_key_fingerprints):
|
||||
"""
|
||||
aka index.jar aka index.xml
|
||||
"""
|
||||
@ -326,12 +369,27 @@ def make_v0(apps, apks, repodir, repodict, requestsdict):
|
||||
|
||||
# Get a list of the apks for this app...
|
||||
apklist = []
|
||||
versionCodes = []
|
||||
apksbyversion = collections.defaultdict(lambda: [])
|
||||
for apk in apks:
|
||||
if apk['packageName'] == appid:
|
||||
if apk['versionCode'] not in versionCodes:
|
||||
apklist.append(apk)
|
||||
versionCodes.append(apk['versionCode'])
|
||||
if apk.get('versionCode') and apk.get('packageName') == appid:
|
||||
apksbyversion[apk['versionCode']].append(apk)
|
||||
for versionCode, apksforver in apksbyversion.items():
|
||||
fdroidsig = fdroid_signing_key_fingerprints.get(appid, {}).get('signer')
|
||||
fdroid_signed_apk = None
|
||||
name_match_apk = None
|
||||
for x in apksforver:
|
||||
if fdroidsig and x.get('signer', None) == fdroidsig:
|
||||
fdroid_signed_apk = x
|
||||
if common.apk_release_filename.match(x.get('apkName', '')):
|
||||
name_match_apk = x
|
||||
# choose which of the available versions is most
|
||||
# suiteable for index v0
|
||||
if fdroid_signed_apk:
|
||||
apklist.append(fdroid_signed_apk)
|
||||
elif name_match_apk:
|
||||
apklist.append(name_match_apk)
|
||||
else:
|
||||
apklist.append(apksforver[0])
|
||||
|
||||
if len(apklist) == 0:
|
||||
continue
|
||||
|
@ -24,19 +24,120 @@ import shutil
|
||||
import glob
|
||||
import hashlib
|
||||
from argparse import ArgumentParser
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
from gettext import ngettext
|
||||
import json
|
||||
import zipfile
|
||||
|
||||
from . import _
|
||||
from . import common
|
||||
from . import metadata
|
||||
from .common import FDroidPopen, SdkToolsPopen
|
||||
from .exception import BuildException
|
||||
from .exception import BuildException, FDroidException
|
||||
|
||||
config = None
|
||||
options = None
|
||||
|
||||
|
||||
def publish_source_tarball(apkfilename, unsigned_dir, output_dir):
|
||||
"""Move the source tarball into the output directory..."""
|
||||
|
||||
tarfilename = apkfilename[:-4] + '_src.tar.gz'
|
||||
tarfile = os.path.join(unsigned_dir, tarfilename)
|
||||
if os.path.exists(tarfile):
|
||||
shutil.move(tarfile, os.path.join(output_dir, tarfilename))
|
||||
logging.debug('...published %s', tarfilename)
|
||||
else:
|
||||
logging.debug('...no source tarball for %s', apkfilename)
|
||||
|
||||
|
||||
def key_alias(appid, resolve=False):
|
||||
"""Get the alias which F-Droid uses to indentify the singing key
|
||||
for this App in F-Droids keystore.
|
||||
"""
|
||||
if config and 'keyaliases' in config and appid in config['keyaliases']:
|
||||
# For this particular app, the key alias is overridden...
|
||||
keyalias = config['keyaliases'][appid]
|
||||
if keyalias.startswith('@'):
|
||||
m = hashlib.md5()
|
||||
m.update(keyalias[1:].encode('utf-8'))
|
||||
keyalias = m.hexdigest()[:8]
|
||||
return keyalias
|
||||
else:
|
||||
m = hashlib.md5()
|
||||
m.update(appid.encode('utf-8'))
|
||||
return m.hexdigest()[:8]
|
||||
|
||||
|
||||
def read_fingerprints_from_keystore():
|
||||
"""Obtain a dictionary containing all singning-key fingerprints which
|
||||
are managed by F-Droid, grouped by appid.
|
||||
"""
|
||||
env_vars = {'LC_ALL': 'C',
|
||||
'FDROID_KEY_STORE_PASS': config['keystorepass'],
|
||||
'FDROID_KEY_PASS': config['keypass']}
|
||||
p = FDroidPopen([config['keytool'], '-list',
|
||||
'-v', '-keystore', config['keystore'],
|
||||
'-storepass:env', 'FDROID_KEY_STORE_PASS'],
|
||||
envs=env_vars, output=False)
|
||||
if p.returncode != 0:
|
||||
raise FDroidException('could not read keysotre {}'.format(config['keystore']))
|
||||
|
||||
realias = re.compile('Alias name: (?P<alias>.+)\n')
|
||||
resha256 = re.compile('\s+SHA256: (?P<sha256>[:0-9A-F]{95})\n')
|
||||
fps = {}
|
||||
for block in p.output.split(('*' * 43) + '\n' + '*' * 43):
|
||||
s_alias = realias.search(block)
|
||||
s_sha256 = resha256.search(block)
|
||||
if s_alias and s_sha256:
|
||||
sigfp = s_sha256.group('sha256').replace(':', '').lower()
|
||||
fps[s_alias.group('alias')] = sigfp
|
||||
return fps
|
||||
|
||||
|
||||
def sign_sig_key_fingerprint_list(jar_file):
|
||||
"""sign the list of app-signing key fingerprints which is
|
||||
used primaryily by fdroid update to determine which APKs
|
||||
where built and signed by F-Droid and which ones were
|
||||
manually added by users.
|
||||
"""
|
||||
cmd = [config['jarsigner']]
|
||||
cmd += '-keystore', config['keystore']
|
||||
cmd += '-storepass:env', 'FDROID_KEY_STORE_PASS'
|
||||
cmd += '-digestalg', 'SHA1'
|
||||
cmd += '-sigalg', 'SHA1withRSA'
|
||||
cmd += jar_file, config['repo_keyalias']
|
||||
if config['keystore'] == 'NONE':
|
||||
cmd += config['smartcardoptions']
|
||||
else: # smardcards never use -keypass
|
||||
cmd += '-keypass:env', 'FDROID_KEY_PASS'
|
||||
env_vars = {'FDROID_KEY_STORE_PASS': config['keystorepass'],
|
||||
'FDROID_KEY_PASS': config['keypass']}
|
||||
p = common.FDroidPopen(cmd, envs=env_vars)
|
||||
if p.returncode != 0:
|
||||
raise FDroidException("Failed to sign '{}'!".format(jar_file))
|
||||
|
||||
|
||||
def store_stats_fdroid_signing_key_fingerprints(appids, indent=None):
|
||||
"""Store list of all signing-key fingerprints for given appids to HD.
|
||||
This list will later on be needed by fdroid update.
|
||||
"""
|
||||
if not os.path.exists('stats'):
|
||||
os.makedirs('stats')
|
||||
data = OrderedDict()
|
||||
fps = read_fingerprints_from_keystore()
|
||||
for appid in sorted(appids):
|
||||
alias = key_alias(appid)
|
||||
if alias in fps:
|
||||
data[appid] = {'signer': fps[key_alias(appid)]}
|
||||
|
||||
jar_file = os.path.join('stats', 'publishsigkeys.jar')
|
||||
with zipfile.ZipFile(jar_file, 'w', zipfile.ZIP_DEFLATED) as jar:
|
||||
jar.writestr('publishsigkeys.json', json.dumps(data, indent=indent))
|
||||
sign_sig_key_fingerprint_list(jar_file)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
global config, options
|
||||
@ -138,22 +239,54 @@ def main():
|
||||
if compare_result:
|
||||
logging.error("...verification failed - publish skipped : "
|
||||
+ compare_result)
|
||||
continue
|
||||
else:
|
||||
|
||||
# Success! So move the downloaded file to the repo, and remove
|
||||
# our built version.
|
||||
shutil.move(srcapk, os.path.join(output_dir, apkfilename))
|
||||
os.remove(apkfile)
|
||||
|
||||
publish_source_tarball(apkfilename, unsigned_dir, output_dir)
|
||||
logging.info('Published ' + apkfilename)
|
||||
|
||||
elif apkfile.endswith('.zip'):
|
||||
|
||||
# OTA ZIPs built by fdroid do not need to be signed by jarsigner,
|
||||
# just to be moved into place in the repo
|
||||
shutil.move(apkfile, os.path.join(output_dir, apkfilename))
|
||||
publish_source_tarball(apkfilename, unsigned_dir, output_dir)
|
||||
logging.info('Published ' + apkfilename)
|
||||
|
||||
else:
|
||||
|
||||
# It's a 'normal' app, i.e. we sign and publish it...
|
||||
skipsigning = False
|
||||
|
||||
# First we handle signatures for this app from local metadata
|
||||
signingfiles = common.metadata_find_developer_signing_files(appid, vercode)
|
||||
if signingfiles:
|
||||
# There's a signature of the app developer present in our
|
||||
# metadata. This means we're going to prepare both a locally
|
||||
# signed APK and a version signed with the developers key.
|
||||
|
||||
signaturefile, signedfile, manifest = signingfiles
|
||||
|
||||
with open(signaturefile, 'rb') as f:
|
||||
devfp = common.signer_fingerprint_short(f.read())
|
||||
devsigned = '{}_{}_{}.apk'.format(appid, vercode, devfp)
|
||||
devsignedtmp = os.path.join(tmp_dir, devsigned)
|
||||
shutil.copy(apkfile, devsignedtmp)
|
||||
|
||||
common.apk_implant_signatures(devsignedtmp, signaturefile,
|
||||
signedfile, manifest)
|
||||
if common.verify_apk_signature(devsignedtmp):
|
||||
shutil.move(devsignedtmp, os.path.join(output_dir, devsigned))
|
||||
else:
|
||||
os.remove(devsignedtmp)
|
||||
logging.error('...verification failed - skipping: %s', devsigned)
|
||||
skipsigning = True
|
||||
|
||||
# Now we sign with the F-Droid key.
|
||||
|
||||
# Figure out the key alias name we'll use. Only the first 8
|
||||
# characters are significant, so we'll use the first 8 from
|
||||
@ -161,6 +294,7 @@ def main():
|
||||
# If a collision does occur later, we're going to have to
|
||||
# come up with a new alogrithm, AND rename all existing keys
|
||||
# in the keystore!
|
||||
if not skipsigning:
|
||||
if appid in config['keyaliases']:
|
||||
# For this particular app, the key alias is overridden...
|
||||
keyalias = config['keyaliases'][appid]
|
||||
@ -194,7 +328,7 @@ def main():
|
||||
'-keypass:env', 'FDROID_KEY_PASS',
|
||||
'-dname', config['keydname']], envs=env_vars)
|
||||
if p.returncode != 0:
|
||||
raise BuildException("Failed to generate key")
|
||||
raise BuildException("Failed to generate key", p.output)
|
||||
|
||||
signed_apk_path = os.path.join(output_dir, apkfilename)
|
||||
if os.path.exists(signed_apk_path):
|
||||
@ -210,7 +344,7 @@ def main():
|
||||
'SHA1withRSA', '-digestalg', 'SHA1',
|
||||
apkfile, keyalias], envs=env_vars)
|
||||
if p.returncode != 0:
|
||||
raise BuildException(_("Failed to sign application"))
|
||||
raise BuildException(_("Failed to sign application"), p.output)
|
||||
|
||||
# Zipalign it...
|
||||
p = SdkToolsPopen(['zipalign', '-v', '4', apkfile,
|
||||
@ -219,14 +353,12 @@ def main():
|
||||
raise BuildException(_("Failed to align application"))
|
||||
os.remove(apkfile)
|
||||
|
||||
# Move the source tarball into the output directory...
|
||||
tarfilename = apkfilename[:-4] + '_src.tar.gz'
|
||||
tarfile = os.path.join(unsigned_dir, tarfilename)
|
||||
if os.path.exists(tarfile):
|
||||
shutil.move(tarfile, os.path.join(output_dir, tarfilename))
|
||||
|
||||
publish_source_tarball(apkfilename, unsigned_dir, output_dir)
|
||||
logging.info('Published ' + apkfilename)
|
||||
|
||||
store_stats_fdroid_signing_key_fingerprints(allapps.keys())
|
||||
logging.info('published list signing-key fingerprints')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
@ -44,7 +44,7 @@ from . import metadata
|
||||
from .common import SdkToolsPopen
|
||||
from .exception import BuildException, FDroidException
|
||||
|
||||
METADATA_VERSION = 18
|
||||
METADATA_VERSION = 19
|
||||
|
||||
# less than the valid range of versionCode, i.e. Java's Integer.MIN_VALUE
|
||||
UNSET_VERSION_CODE = -0x100000000
|
||||
@ -901,7 +901,7 @@ def scan_repo_files(apkcache, repodir, knownapks, use_date_from_file=False):
|
||||
logging.debug("Reading " + name_utf8 + " from cache")
|
||||
usecache = True
|
||||
else:
|
||||
logging.debug("Ignoring stale cache data for " + name)
|
||||
logging.debug("Ignoring stale cache data for " + name_utf8)
|
||||
|
||||
if not usecache:
|
||||
logging.debug(_("Processing {apkfilename}").format(apkfilename=name_utf8))
|
||||
@ -971,11 +971,15 @@ def scan_apk(apk_file):
|
||||
else:
|
||||
scan_apk_androguard(apk, apk_file)
|
||||
|
||||
# Get the signature
|
||||
# Get the signature, or rather the signing key fingerprints
|
||||
logging.debug('Getting signature of {0}'.format(os.path.basename(apk_file)))
|
||||
apk['sig'] = getsig(apk_file)
|
||||
if not apk['sig']:
|
||||
raise BuildException("Failed to get apk signature")
|
||||
apk['signer'] = common.apk_signer_fingerprint(os.path.join(os.getcwd(),
|
||||
apk_file))
|
||||
if not apk.get('signer'):
|
||||
raise BuildException("Failed to get apk signing key fingerprint")
|
||||
|
||||
# Get size of the APK
|
||||
apk['size'] = os.path.getsize(apk_file)
|
||||
|
@ -25,6 +25,7 @@ if localmodule not in sys.path:
|
||||
import fdroidserver.signindex
|
||||
import fdroidserver.common
|
||||
import fdroidserver.metadata
|
||||
from fdroidserver.exception import FDroidException
|
||||
|
||||
|
||||
class CommonTest(unittest.TestCase):
|
||||
@ -376,6 +377,67 @@ class CommonTest(unittest.TestCase):
|
||||
for name in bad:
|
||||
self.assertIsNone(fdroidserver.common.STANDARD_FILE_NAME_REGEX.match(name))
|
||||
|
||||
def test_apk_signer_fingerprint(self):
|
||||
|
||||
# fingerprints fetched with: keytool -printcert -file ____.RSA
|
||||
testapks = (('repo/obb.main.oldversion_1444412523.apk',
|
||||
'818e469465f96b704e27be2fee4c63ab9f83ddf30e7a34c7371a4728d83b0bc1'),
|
||||
('repo/obb.main.twoversions_1101613.apk',
|
||||
'32a23624c201b949f085996ba5ed53d40f703aca4989476949cae891022e0ed6'),
|
||||
('repo/obb.main.twoversions_1101617.apk',
|
||||
'32a23624c201b949f085996ba5ed53d40f703aca4989476949cae891022e0ed6'))
|
||||
|
||||
for apkfile, keytoolcertfingerprint in testapks:
|
||||
self.assertEqual(keytoolcertfingerprint,
|
||||
fdroidserver.common.apk_signer_fingerprint(apkfile))
|
||||
|
||||
def test_apk_signer_fingerprint_short(self):
|
||||
|
||||
# fingerprints fetched with: keytool -printcert -file ____.RSA
|
||||
testapks = (('repo/obb.main.oldversion_1444412523.apk', '818e469'),
|
||||
('repo/obb.main.twoversions_1101613.apk', '32a2362'),
|
||||
('repo/obb.main.twoversions_1101617.apk', '32a2362'))
|
||||
|
||||
for apkfile, keytoolcertfingerprint in testapks:
|
||||
self.assertEqual(keytoolcertfingerprint,
|
||||
fdroidserver.common.apk_signer_fingerprint_short(apkfile))
|
||||
|
||||
def test_get_api_id_aapt(self):
|
||||
|
||||
config = dict()
|
||||
fdroidserver.common.fill_config_defaults(config)
|
||||
fdroidserver.common.config = config
|
||||
self._set_build_tools()
|
||||
config['aapt'] = fdroidserver.common.find_sdk_tools_cmd('aapt')
|
||||
|
||||
appid, vercode, vername = fdroidserver.common.get_apk_id_aapt('repo/obb.main.twoversions_1101613.apk')
|
||||
self.assertEqual('obb.main.twoversions', appid)
|
||||
self.assertEqual('1101613', vercode)
|
||||
self.assertEqual('0.1', vername)
|
||||
|
||||
with self.assertRaises(FDroidException):
|
||||
fdroidserver.common.get_apk_id_aapt('nope')
|
||||
|
||||
def test_apk_release_name(self):
|
||||
appid, vercode, sigfp = fdroidserver.common.apk_parse_release_filename('com.serwylo.lexica_905.apk')
|
||||
self.assertEqual(appid, 'com.serwylo.lexica')
|
||||
self.assertEqual(vercode, '905')
|
||||
self.assertEqual(sigfp, None)
|
||||
|
||||
appid, vercode, sigfp = fdroidserver.common.apk_parse_release_filename('com.serwylo.lexica_905_c82e0f6.apk')
|
||||
self.assertEqual(appid, 'com.serwylo.lexica')
|
||||
self.assertEqual(vercode, '905')
|
||||
self.assertEqual(sigfp, 'c82e0f6')
|
||||
|
||||
appid, vercode, sigfp = fdroidserver.common.apk_parse_release_filename('beverly_hills-90210.apk')
|
||||
self.assertEqual(appid, None)
|
||||
self.assertEqual(vercode, None)
|
||||
self.assertEqual(sigfp, None)
|
||||
|
||||
def test_metadata_find_developer_signature(self):
|
||||
sig = fdroidserver.common.metadata_find_developer_signature('org.smssecure.smssecure')
|
||||
self.assertEqual('b30bb971af0d134866e158ec748fcd553df97c150f58b0a963190bbafbeb0868', sig)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = optparse.OptionParser()
|
||||
|
BIN
tests/dummy-keystore.jks
Normal file
BIN
tests/dummy-keystore.jks
Normal file
Binary file not shown.
@ -7,8 +7,10 @@ import sys
|
||||
import unittest
|
||||
import zipfile
|
||||
from unittest.mock import patch
|
||||
|
||||
import requests
|
||||
import tempfile
|
||||
import json
|
||||
import shutil
|
||||
|
||||
localmodule = os.path.realpath(
|
||||
os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..'))
|
||||
@ -19,6 +21,8 @@ if localmodule not in sys.path:
|
||||
import fdroidserver.common
|
||||
import fdroidserver.index
|
||||
import fdroidserver.signindex
|
||||
import fdroidserver.publish
|
||||
from testcommon import TmpCwd
|
||||
|
||||
|
||||
GP_FINGERPRINT = 'B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135'
|
||||
@ -114,8 +118,117 @@ class IndexTest(unittest.TestCase):
|
||||
self.assertEqual(10, len(index['packages']))
|
||||
self.assertEqual('new_etag', new_etag)
|
||||
|
||||
def test_v1_sort_packages(self):
|
||||
|
||||
i = [{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'org.smssecure.smssecure_134.apk',
|
||||
'signer': 'b33a601a9da97c82e6eb121eb6b90adab561f396602ec4dc8b0019fb587e2af6',
|
||||
'versionCode': 134},
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'org.smssecure.smssecure_134_b30bb97.apk',
|
||||
'signer': 'b30bb971af0d134866e158ec748fcd553df97c150f58b0a963190bbafbeb0868',
|
||||
'versionCode': 134},
|
||||
{'packageName': 'b075b32b4ef1e8a869e00edb136bd48e34a0382b85ced8628f164d1199584e4e'},
|
||||
{'packageName': '43af70d1aca437c2f9974c4634cc5abe45bdc4d5d71529ac4e553488d3bb3ff6'},
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'org.smssecure.smssecure_135_b30bb97.apk',
|
||||
'signer': 'b30bb971af0d134866e158ec748fcd553df97c150f58b0a963190bbafbeb0868',
|
||||
'versionCode': 135},
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'org.smssecure.smssecure_135.apk',
|
||||
'signer': 'b33a601a9da97c82e6eb121eb6b90adab561f396602ec4dc8b0019fb587e2af6',
|
||||
'versionCode': 135},
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'org.smssecure.smssecure_133.apk',
|
||||
'signer': 'b33a601a9da97c82e6eb121eb6b90adab561f396602ec4dc8b0019fb587e2af6',
|
||||
'versionCode': 133},
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'smssecure-weird-version.apk',
|
||||
'signer': '99ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99ff',
|
||||
'versionCode': 133},
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'smssecure-custom.apk',
|
||||
'signer': '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
||||
'versionCode': 133},
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'smssecure-new-custom.apk',
|
||||
'signer': '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
||||
'versionCode': 135}]
|
||||
|
||||
o = [{'packageName': '43af70d1aca437c2f9974c4634cc5abe45bdc4d5d71529ac4e553488d3bb3ff6'},
|
||||
{'packageName': 'b075b32b4ef1e8a869e00edb136bd48e34a0382b85ced8628f164d1199584e4e'},
|
||||
# app test data
|
||||
# # packages with reproducible developer signature
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'org.smssecure.smssecure_135_b30bb97.apk',
|
||||
'signer': 'b30bb971af0d134866e158ec748fcd553df97c150f58b0a963190bbafbeb0868',
|
||||
'versionCode': 135},
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'org.smssecure.smssecure_134_b30bb97.apk',
|
||||
'signer': 'b30bb971af0d134866e158ec748fcd553df97c150f58b0a963190bbafbeb0868',
|
||||
'versionCode': 134},
|
||||
# # packages build and signed by fdroid
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'org.smssecure.smssecure_135.apk',
|
||||
'signer': 'b33a601a9da97c82e6eb121eb6b90adab561f396602ec4dc8b0019fb587e2af6',
|
||||
'versionCode': 135},
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'org.smssecure.smssecure_134.apk',
|
||||
'signer': 'b33a601a9da97c82e6eb121eb6b90adab561f396602ec4dc8b0019fb587e2af6',
|
||||
'versionCode': 134},
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'org.smssecure.smssecure_133.apk',
|
||||
'signer': 'b33a601a9da97c82e6eb121eb6b90adab561f396602ec4dc8b0019fb587e2af6',
|
||||
'versionCode': 133},
|
||||
# # packages signed with unkown keys
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'smssecure-new-custom.apk',
|
||||
'signer': '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
||||
'versionCode': 135},
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'smssecure-custom.apk',
|
||||
'signer': '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
||||
'versionCode': 133},
|
||||
{'packageName': 'org.smssecure.smssecure',
|
||||
'apkName': 'smssecure-weird-version.apk',
|
||||
'signer': '99ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99ff',
|
||||
'versionCode': 133}]
|
||||
|
||||
fdroidserver.common.config = {}
|
||||
fdroidserver.common.fill_config_defaults(fdroidserver.common.config)
|
||||
fdroidserver.publish.config = fdroidserver.common.config
|
||||
fdroidserver.publish.config['keystorepass'] = '123456'
|
||||
fdroidserver.publish.config['keypass'] = '123456'
|
||||
fdroidserver.publish.config['keystore'] = os.path.join(os.getcwd(),
|
||||
'dummy-keystore.jks')
|
||||
fdroidserver.publish.config['repo_keyalias'] = 'repokey'
|
||||
|
||||
testsmetadir = os.path.join(os.getcwd(), 'metadata')
|
||||
with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir):
|
||||
shutil.copytree(testsmetadir, 'metadata')
|
||||
sigkeyfps = {
|
||||
"org.smssecure.smssecure": {
|
||||
"signer": "b33a601a9da97c82e6eb121eb6b90adab561f396602ec4dc8b0019fb587e2af6"
|
||||
}
|
||||
}
|
||||
os.makedirs('stats')
|
||||
jarfile = 'stats/publishsigkeys.jar'
|
||||
with zipfile.ZipFile(jarfile, 'w', zipfile.ZIP_DEFLATED) as jar:
|
||||
jar.writestr('publishsigkeys.json', json.dumps(sigkeyfps))
|
||||
fdroidserver.publish.sign_sig_key_fingerprint_list(jarfile)
|
||||
with open('config.py', 'w'):
|
||||
pass
|
||||
|
||||
fdroidserver.index.v1_sort_packages(
|
||||
i, 'repo', fdroidserver.common.load_stats_fdroid_signing_key_fingerprints())
|
||||
self.maxDiff = None
|
||||
self.assertEqual(json.dumps(i, indent=2), json.dumps(o, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if os.path.basename(os.getcwd()) != 'tests' and os.path.isdir('tests'):
|
||||
os.chdir('tests')
|
||||
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option("-v", "--verbose", action="store_true", default=False,
|
||||
help="Spew out even more information than normal")
|
||||
|
@ -12,6 +12,7 @@ icons_src:
|
||||
minSdkVersion: '4'
|
||||
packageName: info.guardianproject.urzip
|
||||
sig: e0ecb5fc2d63088e4a07ae410a127722
|
||||
signer: 7eabd8c15de883d1e82b5df2fd4f7f769e498078e9ad6dc901f0e96db77ceac3
|
||||
size: 9969
|
||||
targetSdkVersion: '18'
|
||||
uses-permission: []
|
||||
|
@ -24,6 +24,7 @@ nativecode:
|
||||
- x86_64
|
||||
packageName: org.dyndns.fules.ck
|
||||
sig: 9bf7a6a67f95688daec75eab4b1436ac
|
||||
signer: 9326a2cc1a2f148202bc7837a0af3b81200bd37fd359c9e13a2296a71d342056
|
||||
size: 132453
|
||||
targetSdkVersion: '8'
|
||||
uses-permission:
|
||||
|
Binary file not shown.
6837
tests/metadata/org.smssecure.smssecure/signatures/134/28969C09.SF
Normal file
6837
tests/metadata/org.smssecure.smssecure/signatures/134/28969C09.SF
Normal file
File diff suppressed because it is too large
Load Diff
6835
tests/metadata/org.smssecure.smssecure/signatures/134/MANIFEST.MF
Normal file
6835
tests/metadata/org.smssecure.smssecure/signatures/134/MANIFEST.MF
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
6800
tests/metadata/org.smssecure.smssecure/signatures/135/28969C09.SF
Normal file
6800
tests/metadata/org.smssecure.smssecure/signatures/135/28969C09.SF
Normal file
File diff suppressed because it is too large
Load Diff
6799
tests/metadata/org.smssecure.smssecure/signatures/135/MANIFEST.MF
Normal file
6799
tests/metadata/org.smssecure.smssecure/signatures/135/MANIFEST.MF
Normal file
File diff suppressed because it is too large
Load Diff
147
tests/publish.TestCase
Executable file
147
tests/publish.TestCase
Executable file
@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
#
|
||||
# command which created the keystore used in this test case:
|
||||
#
|
||||
# $ for ALIAS in 'repokey a163ec9b d2d51ff2 dc3b169e 78688a0f'; \
|
||||
# do keytool -genkey -keystore dummy-keystore.jks \
|
||||
# -alias $ALIAS -keyalg 'RSA' -keysize '2048' \
|
||||
# -validity '10000' -storepass 123456 \
|
||||
# -keypass 123456 -dname 'CN=test, OU=F-Droid'; done
|
||||
#
|
||||
|
||||
import inspect
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import tempfile
|
||||
import textwrap
|
||||
|
||||
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 fdroidserver import publish
|
||||
from fdroidserver import common
|
||||
from fdroidserver.exception import FDroidException
|
||||
|
||||
|
||||
class PublishTest(unittest.TestCase):
|
||||
'''fdroidserver/publish.py'''
|
||||
|
||||
def test_key_alias(self):
|
||||
publish.config = {}
|
||||
self.assertEqual('a163ec9b', publish.key_alias('com.example.app'))
|
||||
self.assertEqual('d2d51ff2', publish.key_alias('com.example.anotherapp'))
|
||||
self.assertEqual('dc3b169e', publish.key_alias('org.test.testy'))
|
||||
self.assertEqual('78688a0f', publish.key_alias('org.org.org'))
|
||||
|
||||
publish.config = {'keyaliases': {'yep.app': '@org.org.org',
|
||||
'com.example.app': '1a2b3c4d'}}
|
||||
self.assertEqual('78688a0f', publish.key_alias('yep.app'))
|
||||
self.assertEqual('1a2b3c4d', publish.key_alias('com.example.app'))
|
||||
|
||||
def test_read_fingerprints_from_keystore(self):
|
||||
common.config = {}
|
||||
common.fill_config_defaults(common.config)
|
||||
publish.config = common.config
|
||||
publish.config['keystorepass'] = '123456'
|
||||
publish.config['keypass'] = '123456'
|
||||
publish.config['keystore'] = 'dummy-keystore.jks'
|
||||
|
||||
expected = {'78688a0f': '277655a6235bc6b0ef2d824396c51ba947f5ebc738c293d887e7083ff338af82',
|
||||
'd2d51ff2': 'fa3f6a017541ee7fe797be084b1bcfbf92418a7589ef1f7fdeb46741b6d2e9c3',
|
||||
'dc3b169e': '6ae5355157a47ddcc3834a71f57f6fb5a8c2621c8e0dc739e9ddf59f865e497c',
|
||||
'a163ec9b': 'd34f678afbaa8f2fa6cc0edd6f0c2d1d2e2e9eb08bea521b24c740806016bff4',
|
||||
'repokey': 'c58460800c7b250a619c30c13b07b7359a43e5af71a4352d86c58ae18c9f6d41'}
|
||||
result = publish.read_fingerprints_from_keystore()
|
||||
self.maxDiff = None
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_store_and_load_fdroid_signing_key_fingerprints(self):
|
||||
common.config = {}
|
||||
common.fill_config_defaults(common.config)
|
||||
publish.config = common.config
|
||||
publish.config['keystorepass'] = '123456'
|
||||
publish.config['keypass'] = '123456'
|
||||
publish.config['keystore'] = os.path.join(os.getcwd(),
|
||||
'dummy-keystore.jks')
|
||||
publish.config['repo_keyalias'] = 'repokey'
|
||||
|
||||
appids = ['com.example.app',
|
||||
'net.unavailable',
|
||||
'org.test.testy',
|
||||
'com.example.anotherapp',
|
||||
'org.org.org']
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
orig_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmpdir)
|
||||
with open('config.py', 'w') as f:
|
||||
pass
|
||||
|
||||
publish.store_stats_fdroid_signing_key_fingerprints(appids, indent=2)
|
||||
|
||||
self.maxDiff = None
|
||||
expected = {
|
||||
"com.example.anotherapp": {
|
||||
"signer": "fa3f6a017541ee7fe797be084b1bcfbf92418a7589ef1f7fdeb46741b6d2e9c3"
|
||||
},
|
||||
"com.example.app": {
|
||||
"signer": "d34f678afbaa8f2fa6cc0edd6f0c2d1d2e2e9eb08bea521b24c740806016bff4"
|
||||
},
|
||||
"org.org.org": {
|
||||
"signer": "277655a6235bc6b0ef2d824396c51ba947f5ebc738c293d887e7083ff338af82"
|
||||
},
|
||||
"org.test.testy": {
|
||||
"signer": "6ae5355157a47ddcc3834a71f57f6fb5a8c2621c8e0dc739e9ddf59f865e497c"
|
||||
}
|
||||
}
|
||||
self.assertEqual(expected, common.load_stats_fdroid_signing_key_fingerprints())
|
||||
|
||||
with open('config.py', 'r') as f:
|
||||
self.assertEqual(textwrap.dedent('''\
|
||||
|
||||
repo_key_sha256 = "c58460800c7b250a619c30c13b07b7359a43e5af71a4352d86c58ae18c9f6d41"
|
||||
'''), f.read())
|
||||
finally:
|
||||
os.chdir(orig_cwd)
|
||||
|
||||
def test_store_and_load_fdroid_signing_key_fingerprints_with_missmatch(self):
|
||||
common.config = {}
|
||||
common.fill_config_defaults(common.config)
|
||||
publish.config = common.config
|
||||
publish.config['keystorepass'] = '123456'
|
||||
publish.config['keypass'] = '123456'
|
||||
publish.config['keystore'] = os.path.join(os.getcwd(),
|
||||
'dummy-keystore.jks')
|
||||
publish.config['repo_keyalias'] = 'repokey'
|
||||
publish.config['repo_key_sha256'] = 'bad bad bad bad bad bad bad bad bad bad bad bad'
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
orig_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmpdir)
|
||||
publish.store_stats_fdroid_signing_key_fingerprints({}, indent=2)
|
||||
with self.assertRaises(FDroidException):
|
||||
common.load_stats_fdroid_signing_key_fingerprints()
|
||||
finally:
|
||||
os.chdir(orig_cwd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if os.path.basename(os.getcwd()) != 'tests' and os.path.isdir('tests'):
|
||||
os.chdir('tests')
|
||||
|
||||
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(PublishTest))
|
||||
unittest.main()
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<fdroid>
|
||||
<repo icon="fdroid-icon.png" name="My First F-Droid Repo Demo" pubkey="308204e1308202c9a003020102020434597643300d06092a864886f70d01010b050030213110300e060355040b1307462d44726f6964310d300b06035504031304736f7661301e170d3136303931333230313930395a170d3434303133303230313930395a30213110300e060355040b1307462d44726f6964310d300b06035504031304736f766130820222300d06092a864886f70d01010105000382020f003082020a028202010086ef94b5aacf2ba4f38c875f4194b44f5644392e3715575d7c92828577e692c352b567172823851c8c72347fbc9d99684cd7ca3e1db3e4cca126382c53f2a5869fb4c19bdec989b2930501af3e758ff40588915fe96b10076ce3346a193a0277d79e83e30fd8657c20e35260dd085aa32eac7c4b85786ffefbf1555cafe2bc928443430cdbba48cfbe701e12ae86e676477932730d4fc7c00af820aef85038a5b4df084cf6470d110dc4c49ea1b749b80b34709d199b3db516b223625c5de4501e861f7d261b3838f8f616aa78831d618d41d25872dc810c9b2087b5a9e146ca95be740316dcdbcb77314e23ab87d4487913b800b1113c0603ea2294188b71d3e49875df097b56f9151211fc6832f9790c5c83d17481f14ad37915fd164f4fd713f6732a15f4245714b84cd665bdbd085660ea33ad7d7095dcc414f09e3903604a40facc2314a115c0045bb50e9df38efb57e1b8e7cc105f340a26eeb46aba0fa6672953eee7f1f92dcb408e561909bbd4bdf4a4948c4d57c467d21aa238c34ba43be050398be963191fa2b49828bc1e4eeed224b40dbe9dc3e570890a71a974a2f4527edb1b07105071755105edcb2af2f269facfb89180903a572a99b46456e80d4a01685a80b233278805f2c876678e731f4ec4f52075aeef6b2b023efbb8a3637ef507c4c37c27e428152ec1817fcba640ad601cb09f72f0fbe2d274a2410203010001a321301f301d0603551d0e04160414c28bf33dd5a9a17338e5b1d1a6edd8c7d141ed0b300d06092a864886f70d01010b0500038202010084e20458b2aafd7fc27146b0986f9324f4260f244920417a77c9bf15e2e2d22d2725bdd8093ec261c3779c3ca03312516506f9410075b90595b41345956d8eb2786fb5994f195611382c2b99dba13381b0100a30bc9e6e47248bf4325e2f6eec9d789216dc7536e753bf1f4be603d9fa2e6f5e192b4eb988b8cdb0bb1e8668a9225426f7d4636479f73ed24ad1d2657c31e63c93d9679b9080171b3bd1bf10a3b92b80bd790fbf62d3644900cd08eae8b9bf9c2567be98dc8cdd2ae19a8d57a3e3e2de899f81f1279f578989e6af906f80c8c2b67651730ee7e568c1af5bcb845b6d685dc55332a9984aeceaea3b7e883447edf1c76b155d95253e39b9710eaa22efa6c81468829702b5dce7126538f3ca70c2f0ad9a5795435fdb1f715f20d60359ef9a9926c7050116e802df651727447848827815f70bd82af3cedd08783156102d2d8ce995c4c43b8e47e91a3e6927f3505a5d395e6bebb84542c570903eeab4382a1c2151f1471c7a06a34dc4d268d8fa72e93bdcd2dccc4302ecac47b9e7e3d8bc9b46d21cd097874a24d529548018dc190ff568c6aa428f0a5eedff1a347730931c74f19277538e49647a4ad7254f4c1ec7d4da12cce9e1fad9607534e66ab40a56b473d9d7e3d563fd03cad2052bad365c5a29f8ae54f09b60dbca3ea768d7767cbe1c133ca08ce725c1c1370f4aab8e5b6e286f52dc0be8d0982b5a" timestamp="1480431575" url="https://MyFirstFDroidRepo.org/fdroid/repo" version="18">
|
||||
<repo icon="fdroid-icon.png" name="My First F-Droid Repo Demo" pubkey="308204e1308202c9a003020102020434597643300d06092a864886f70d01010b050030213110300e060355040b1307462d44726f6964310d300b06035504031304736f7661301e170d3136303931333230313930395a170d3434303133303230313930395a30213110300e060355040b1307462d44726f6964310d300b06035504031304736f766130820222300d06092a864886f70d01010105000382020f003082020a028202010086ef94b5aacf2ba4f38c875f4194b44f5644392e3715575d7c92828577e692c352b567172823851c8c72347fbc9d99684cd7ca3e1db3e4cca126382c53f2a5869fb4c19bdec989b2930501af3e758ff40588915fe96b10076ce3346a193a0277d79e83e30fd8657c20e35260dd085aa32eac7c4b85786ffefbf1555cafe2bc928443430cdbba48cfbe701e12ae86e676477932730d4fc7c00af820aef85038a5b4df084cf6470d110dc4c49ea1b749b80b34709d199b3db516b223625c5de4501e861f7d261b3838f8f616aa78831d618d41d25872dc810c9b2087b5a9e146ca95be740316dcdbcb77314e23ab87d4487913b800b1113c0603ea2294188b71d3e49875df097b56f9151211fc6832f9790c5c83d17481f14ad37915fd164f4fd713f6732a15f4245714b84cd665bdbd085660ea33ad7d7095dcc414f09e3903604a40facc2314a115c0045bb50e9df38efb57e1b8e7cc105f340a26eeb46aba0fa6672953eee7f1f92dcb408e561909bbd4bdf4a4948c4d57c467d21aa238c34ba43be050398be963191fa2b49828bc1e4eeed224b40dbe9dc3e570890a71a974a2f4527edb1b07105071755105edcb2af2f269facfb89180903a572a99b46456e80d4a01685a80b233278805f2c876678e731f4ec4f52075aeef6b2b023efbb8a3637ef507c4c37c27e428152ec1817fcba640ad601cb09f72f0fbe2d274a2410203010001a321301f301d0603551d0e04160414c28bf33dd5a9a17338e5b1d1a6edd8c7d141ed0b300d06092a864886f70d01010b0500038202010084e20458b2aafd7fc27146b0986f9324f4260f244920417a77c9bf15e2e2d22d2725bdd8093ec261c3779c3ca03312516506f9410075b90595b41345956d8eb2786fb5994f195611382c2b99dba13381b0100a30bc9e6e47248bf4325e2f6eec9d789216dc7536e753bf1f4be603d9fa2e6f5e192b4eb988b8cdb0bb1e8668a9225426f7d4636479f73ed24ad1d2657c31e63c93d9679b9080171b3bd1bf10a3b92b80bd790fbf62d3644900cd08eae8b9bf9c2567be98dc8cdd2ae19a8d57a3e3e2de899f81f1279f578989e6af906f80c8c2b67651730ee7e568c1af5bcb845b6d685dc55332a9984aeceaea3b7e883447edf1c76b155d95253e39b9710eaa22efa6c81468829702b5dce7126538f3ca70c2f0ad9a5795435fdb1f715f20d60359ef9a9926c7050116e802df651727447848827815f70bd82af3cedd08783156102d2d8ce995c4c43b8e47e91a3e6927f3505a5d395e6bebb84542c570903eeab4382a1c2151f1471c7a06a34dc4d268d8fa72e93bdcd2dccc4302ecac47b9e7e3d8bc9b46d21cd097874a24d529548018dc190ff568c6aa428f0a5eedff1a347730931c74f19277538e49647a4ad7254f4c1ec7d4da12cce9e1fad9607534e66ab40a56b473d9d7e3d563fd03cad2052bad365c5a29f8ae54f09b60dbca3ea768d7767cbe1c133ca08ce725c1c1370f4aab8e5b6e286f52dc0be8d0982b5a" timestamp="1480431575" url="https://MyFirstFDroidRepo.org/fdroid/repo" version="19">
|
||||
<description>This is a repository of apps to be used with F-Droid. Applications in this repository are either official binaries built by the original application developers, or are binaries built from source by the admin of f-droid.org using the tools on https://gitlab.com/u/fdroid. </description>
|
||||
<mirror>http://foobarfoobarfoobar.onion/fdroid/repo</mirror>
|
||||
<mirror>https://foo.bar/fdroid/repo</mirror>
|
||||
|
Loading…
Reference in New Issue
Block a user