mirror of
https://gitlab.com/fdroid/fdroidserver.git
synced 2024-10-06 19:10:12 +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 hashlib
|
||||||
import socket
|
import socket
|
||||||
import base64
|
import base64
|
||||||
|
import zipfile
|
||||||
|
import tempfile
|
||||||
|
import json
|
||||||
import xml.etree.ElementTree as XMLElementTree
|
import xml.etree.ElementTree as XMLElementTree
|
||||||
|
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
@ -509,6 +512,32 @@ def publishednameinfo(filename):
|
|||||||
return result
|
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):
|
def get_release_filename(app, build):
|
||||||
if build.output:
|
if build.output:
|
||||||
return "%s_%s.%s" % (app.id, build.versionCode, get_file_extension(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)')
|
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):
|
def metadata_get_sigdir(appid, vercode=None):
|
||||||
"""Get signature directory for app"""
|
"""Get signature directory for app"""
|
||||||
if vercode:
|
if vercode:
|
||||||
@ -2022,6 +2112,128 @@ def metadata_get_sigdir(appid, vercode=None):
|
|||||||
return os.path.join('metadata', appid, 'signatures')
|
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):
|
def apk_extract_signatures(apkpath, outdir, manifest=True):
|
||||||
"""Extracts a signature files from APK and puts them into target directory.
|
"""Extracts a signature files from APK and puts them into target directory.
|
||||||
|
|
||||||
@ -2061,30 +2273,35 @@ def verify_apks(signed_apk, unsigned_apk, tmp_dir):
|
|||||||
describing what went wrong.
|
describing what went wrong.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
signed = ZipFile(signed_apk, 'r')
|
if not os.path.isfile(signed_apk):
|
||||||
meta_inf_files = ['META-INF/MANIFEST.MF']
|
return 'can not verify: file does not exists: {}'.format(signed_apk)
|
||||||
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))
|
if not os.path.isfile(unsigned_apk):
|
||||||
unsigned = ZipFile(unsigned_apk, 'r')
|
return 'can not verify: file does not exists: {}'.format(unsigned_apk)
|
||||||
# only read the signature from the signed APK, everything else from unsigned
|
|
||||||
with ZipFile(tmp_apk, 'w') as tmp:
|
with ZipFile(signed_apk, 'r') as signed:
|
||||||
for filename in meta_inf_files:
|
meta_inf_files = ['META-INF/MANIFEST.MF']
|
||||||
tmp.writestr(signed.getinfo(filename), signed.read(filename))
|
for f in signed.namelist():
|
||||||
for info in unsigned.infolist():
|
if apk_sigfile.match(f) \
|
||||||
if info.filename in meta_inf_files:
|
or f in ['META-INF/fdroidserverid', 'META-INF/buildserverid']:
|
||||||
logging.warning('Ignoring ' + info.filename + ' from ' + unsigned_apk)
|
meta_inf_files.append(f)
|
||||||
continue
|
if len(meta_inf_files) < 3:
|
||||||
if info.filename in tmp.namelist():
|
return "Signature files missing from {0}".format(signed_apk)
|
||||||
return "duplicate filename found: " + info.filename
|
|
||||||
tmp.writestr(info, unsigned.read(info.filename))
|
tmp_apk = os.path.join(tmp_dir, 'sigcp_' + os.path.basename(unsigned_apk))
|
||||||
unsigned.close()
|
with ZipFile(unsigned_apk, 'r') as unsigned:
|
||||||
signed.close()
|
# 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 %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))
|
||||||
|
|
||||||
verified = verify_apk_signature(tmp_apk)
|
verified = verify_apk_signature(tmp_apk)
|
||||||
|
|
||||||
@ -2348,6 +2565,34 @@ def get_certificate(certificate_file):
|
|||||||
return encoder.encode(cert)
|
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):
|
def write_to_config(thisconfig, key, value=None, config_file=None):
|
||||||
'''write a key/value to the local config.py
|
'''write a key/value to the local config.py
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ from . import common
|
|||||||
from . import metadata
|
from . import metadata
|
||||||
from . import net
|
from . import net
|
||||||
from . import signindex
|
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
|
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'))
|
raise TypeError(_('only accepts strings, lists, and tuples'))
|
||||||
requestsdict[command] = packageNames
|
requestsdict[command] = packageNames
|
||||||
|
|
||||||
make_v0(appsWithPackages, apks, repodir, repodict, requestsdict)
|
fdroid_signing_key_fingerprints = load_stats_fdroid_signing_key_fingerprints()
|
||||||
make_v1(appsWithPackages, apks, repodir, repodict, requestsdict)
|
|
||||||
|
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):
|
def _index_encoder_default(obj):
|
||||||
if isinstance(obj, set):
|
if isinstance(obj, set):
|
||||||
@ -168,6 +172,9 @@ def make_v1(apps, packages, repodir, repodict, requestsdict):
|
|||||||
output['repo'] = repodict
|
output['repo'] = repodict
|
||||||
output['requests'] = requestsdict
|
output['requests'] = requestsdict
|
||||||
|
|
||||||
|
# establish sort order of the index
|
||||||
|
v1_sort_packages(packages, repodir, fdroid_signing_key_fingerprints)
|
||||||
|
|
||||||
appslist = []
|
appslist = []
|
||||||
output['apps'] = appslist
|
output['apps'] = appslist
|
||||||
for packageName, appdict in apps.items():
|
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)
|
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
|
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...
|
# Get a list of the apks for this app...
|
||||||
apklist = []
|
apklist = []
|
||||||
versionCodes = []
|
apksbyversion = collections.defaultdict(lambda: [])
|
||||||
for apk in apks:
|
for apk in apks:
|
||||||
if apk['packageName'] == appid:
|
if apk.get('versionCode') and apk.get('packageName') == appid:
|
||||||
if apk['versionCode'] not in versionCodes:
|
apksbyversion[apk['versionCode']].append(apk)
|
||||||
apklist.append(apk)
|
for versionCode, apksforver in apksbyversion.items():
|
||||||
versionCodes.append(apk['versionCode'])
|
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:
|
if len(apklist) == 0:
|
||||||
continue
|
continue
|
||||||
|
@ -24,19 +24,120 @@ import shutil
|
|||||||
import glob
|
import glob
|
||||||
import hashlib
|
import hashlib
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
|
from collections import OrderedDict
|
||||||
import logging
|
import logging
|
||||||
from gettext import ngettext
|
from gettext import ngettext
|
||||||
|
import json
|
||||||
|
import zipfile
|
||||||
|
|
||||||
from . import _
|
from . import _
|
||||||
from . import common
|
from . import common
|
||||||
from . import metadata
|
from . import metadata
|
||||||
from .common import FDroidPopen, SdkToolsPopen
|
from .common import FDroidPopen, SdkToolsPopen
|
||||||
from .exception import BuildException
|
from .exception import BuildException, FDroidException
|
||||||
|
|
||||||
config = None
|
config = None
|
||||||
options = 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():
|
def main():
|
||||||
|
|
||||||
global config, options
|
global config, options
|
||||||
@ -138,22 +239,54 @@ def main():
|
|||||||
if compare_result:
|
if compare_result:
|
||||||
logging.error("...verification failed - publish skipped : "
|
logging.error("...verification failed - publish skipped : "
|
||||||
+ compare_result)
|
+ compare_result)
|
||||||
continue
|
else:
|
||||||
|
|
||||||
# Success! So move the downloaded file to the repo, and remove
|
# Success! So move the downloaded file to the repo, and remove
|
||||||
# our built version.
|
# our built version.
|
||||||
shutil.move(srcapk, os.path.join(output_dir, apkfilename))
|
shutil.move(srcapk, os.path.join(output_dir, apkfilename))
|
||||||
os.remove(apkfile)
|
os.remove(apkfile)
|
||||||
|
|
||||||
|
publish_source_tarball(apkfilename, unsigned_dir, output_dir)
|
||||||
|
logging.info('Published ' + apkfilename)
|
||||||
|
|
||||||
elif apkfile.endswith('.zip'):
|
elif apkfile.endswith('.zip'):
|
||||||
|
|
||||||
# OTA ZIPs built by fdroid do not need to be signed by jarsigner,
|
# OTA ZIPs built by fdroid do not need to be signed by jarsigner,
|
||||||
# just to be moved into place in the repo
|
# just to be moved into place in the repo
|
||||||
shutil.move(apkfile, os.path.join(output_dir, apkfilename))
|
shutil.move(apkfile, os.path.join(output_dir, apkfilename))
|
||||||
|
publish_source_tarball(apkfilename, unsigned_dir, output_dir)
|
||||||
|
logging.info('Published ' + apkfilename)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
# It's a 'normal' app, i.e. we sign and publish it...
|
# 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
|
# Figure out the key alias name we'll use. Only the first 8
|
||||||
# characters are significant, so we'll use the first 8 from
|
# characters are significant, so we'll use the first 8 from
|
||||||
@ -161,71 +294,70 @@ def main():
|
|||||||
# If a collision does occur later, we're going to have to
|
# If a collision does occur later, we're going to have to
|
||||||
# come up with a new alogrithm, AND rename all existing keys
|
# come up with a new alogrithm, AND rename all existing keys
|
||||||
# in the keystore!
|
# in the keystore!
|
||||||
if appid in config['keyaliases']:
|
if not skipsigning:
|
||||||
# For this particular app, the key alias is overridden...
|
if appid in config['keyaliases']:
|
||||||
keyalias = config['keyaliases'][appid]
|
# For this particular app, the key alias is overridden...
|
||||||
if keyalias.startswith('@'):
|
keyalias = config['keyaliases'][appid]
|
||||||
|
if keyalias.startswith('@'):
|
||||||
|
m = hashlib.md5()
|
||||||
|
m.update(keyalias[1:].encode('utf-8'))
|
||||||
|
keyalias = m.hexdigest()[:8]
|
||||||
|
else:
|
||||||
m = hashlib.md5()
|
m = hashlib.md5()
|
||||||
m.update(keyalias[1:].encode('utf-8'))
|
m.update(appid.encode('utf-8'))
|
||||||
keyalias = m.hexdigest()[:8]
|
keyalias = m.hexdigest()[:8]
|
||||||
else:
|
logging.info("Key alias: " + keyalias)
|
||||||
m = hashlib.md5()
|
|
||||||
m.update(appid.encode('utf-8'))
|
|
||||||
keyalias = m.hexdigest()[:8]
|
|
||||||
logging.info("Key alias: " + keyalias)
|
|
||||||
|
|
||||||
# See if we already have a key for this application, and
|
# See if we already have a key for this application, and
|
||||||
# if not generate one...
|
# if not generate one...
|
||||||
env_vars = {
|
env_vars = {
|
||||||
'FDROID_KEY_STORE_PASS': config['keystorepass'],
|
'FDROID_KEY_STORE_PASS': config['keystorepass'],
|
||||||
'FDROID_KEY_PASS': config['keypass'],
|
'FDROID_KEY_PASS': config['keypass'],
|
||||||
}
|
}
|
||||||
p = FDroidPopen([config['keytool'], '-list',
|
p = FDroidPopen([config['keytool'], '-list',
|
||||||
'-alias', keyalias, '-keystore', config['keystore'],
|
'-alias', keyalias, '-keystore', config['keystore'],
|
||||||
'-storepass:env', 'FDROID_KEY_STORE_PASS'], envs=env_vars)
|
'-storepass:env', 'FDROID_KEY_STORE_PASS'], envs=env_vars)
|
||||||
if p.returncode != 0:
|
|
||||||
logging.info("Key does not exist - generating...")
|
|
||||||
p = FDroidPopen([config['keytool'], '-genkey',
|
|
||||||
'-keystore', config['keystore'],
|
|
||||||
'-alias', keyalias,
|
|
||||||
'-keyalg', 'RSA', '-keysize', '2048',
|
|
||||||
'-validity', '10000',
|
|
||||||
'-storepass:env', 'FDROID_KEY_STORE_PASS',
|
|
||||||
'-keypass:env', 'FDROID_KEY_PASS',
|
|
||||||
'-dname', config['keydname']], envs=env_vars)
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise BuildException("Failed to generate key")
|
logging.info("Key does not exist - generating...")
|
||||||
|
p = FDroidPopen([config['keytool'], '-genkey',
|
||||||
|
'-keystore', config['keystore'],
|
||||||
|
'-alias', keyalias,
|
||||||
|
'-keyalg', 'RSA', '-keysize', '2048',
|
||||||
|
'-validity', '10000',
|
||||||
|
'-storepass:env', 'FDROID_KEY_STORE_PASS',
|
||||||
|
'-keypass:env', 'FDROID_KEY_PASS',
|
||||||
|
'-dname', config['keydname']], envs=env_vars)
|
||||||
|
if p.returncode != 0:
|
||||||
|
raise BuildException("Failed to generate key", p.output)
|
||||||
|
|
||||||
signed_apk_path = os.path.join(output_dir, apkfilename)
|
signed_apk_path = os.path.join(output_dir, apkfilename)
|
||||||
if os.path.exists(signed_apk_path):
|
if os.path.exists(signed_apk_path):
|
||||||
raise BuildException("Refusing to sign '{0}' file exists in both "
|
raise BuildException("Refusing to sign '{0}' file exists in both "
|
||||||
"{1} and {2} folder.".format(apkfilename,
|
"{1} and {2} folder.".format(apkfilename,
|
||||||
unsigned_dir,
|
unsigned_dir,
|
||||||
output_dir))
|
output_dir))
|
||||||
|
|
||||||
# Sign the application...
|
# Sign the application...
|
||||||
p = FDroidPopen([config['jarsigner'], '-keystore', config['keystore'],
|
p = FDroidPopen([config['jarsigner'], '-keystore', config['keystore'],
|
||||||
'-storepass:env', 'FDROID_KEY_STORE_PASS',
|
'-storepass:env', 'FDROID_KEY_STORE_PASS',
|
||||||
'-keypass:env', 'FDROID_KEY_PASS', '-sigalg',
|
'-keypass:env', 'FDROID_KEY_PASS', '-sigalg',
|
||||||
'SHA1withRSA', '-digestalg', 'SHA1',
|
'SHA1withRSA', '-digestalg', 'SHA1',
|
||||||
apkfile, keyalias], envs=env_vars)
|
apkfile, keyalias], envs=env_vars)
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise BuildException(_("Failed to sign application"))
|
raise BuildException(_("Failed to sign application"), p.output)
|
||||||
|
|
||||||
# Zipalign it...
|
# Zipalign it...
|
||||||
p = SdkToolsPopen(['zipalign', '-v', '4', apkfile,
|
p = SdkToolsPopen(['zipalign', '-v', '4', apkfile,
|
||||||
os.path.join(output_dir, apkfilename)])
|
os.path.join(output_dir, apkfilename)])
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise BuildException(_("Failed to align application"))
|
raise BuildException(_("Failed to align application"))
|
||||||
os.remove(apkfile)
|
os.remove(apkfile)
|
||||||
|
|
||||||
# Move the source tarball into the output directory...
|
publish_source_tarball(apkfilename, unsigned_dir, output_dir)
|
||||||
tarfilename = apkfilename[:-4] + '_src.tar.gz'
|
logging.info('Published ' + apkfilename)
|
||||||
tarfile = os.path.join(unsigned_dir, tarfilename)
|
|
||||||
if os.path.exists(tarfile):
|
|
||||||
shutil.move(tarfile, os.path.join(output_dir, tarfilename))
|
|
||||||
|
|
||||||
logging.info('Published ' + apkfilename)
|
store_stats_fdroid_signing_key_fingerprints(allapps.keys())
|
||||||
|
logging.info('published list signing-key fingerprints')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -44,7 +44,7 @@ from . import metadata
|
|||||||
from .common import SdkToolsPopen
|
from .common import SdkToolsPopen
|
||||||
from .exception import BuildException, FDroidException
|
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
|
# less than the valid range of versionCode, i.e. Java's Integer.MIN_VALUE
|
||||||
UNSET_VERSION_CODE = -0x100000000
|
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")
|
logging.debug("Reading " + name_utf8 + " from cache")
|
||||||
usecache = True
|
usecache = True
|
||||||
else:
|
else:
|
||||||
logging.debug("Ignoring stale cache data for " + name)
|
logging.debug("Ignoring stale cache data for " + name_utf8)
|
||||||
|
|
||||||
if not usecache:
|
if not usecache:
|
||||||
logging.debug(_("Processing {apkfilename}").format(apkfilename=name_utf8))
|
logging.debug(_("Processing {apkfilename}").format(apkfilename=name_utf8))
|
||||||
@ -971,11 +971,15 @@ def scan_apk(apk_file):
|
|||||||
else:
|
else:
|
||||||
scan_apk_androguard(apk, apk_file)
|
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)))
|
logging.debug('Getting signature of {0}'.format(os.path.basename(apk_file)))
|
||||||
apk['sig'] = getsig(apk_file)
|
apk['sig'] = getsig(apk_file)
|
||||||
if not apk['sig']:
|
if not apk['sig']:
|
||||||
raise BuildException("Failed to get apk signature")
|
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
|
# Get size of the APK
|
||||||
apk['size'] = os.path.getsize(apk_file)
|
apk['size'] = os.path.getsize(apk_file)
|
||||||
|
@ -25,6 +25,7 @@ if localmodule not in sys.path:
|
|||||||
import fdroidserver.signindex
|
import fdroidserver.signindex
|
||||||
import fdroidserver.common
|
import fdroidserver.common
|
||||||
import fdroidserver.metadata
|
import fdroidserver.metadata
|
||||||
|
from fdroidserver.exception import FDroidException
|
||||||
|
|
||||||
|
|
||||||
class CommonTest(unittest.TestCase):
|
class CommonTest(unittest.TestCase):
|
||||||
@ -376,6 +377,67 @@ class CommonTest(unittest.TestCase):
|
|||||||
for name in bad:
|
for name in bad:
|
||||||
self.assertIsNone(fdroidserver.common.STANDARD_FILE_NAME_REGEX.match(name))
|
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__":
|
if __name__ == "__main__":
|
||||||
parser = optparse.OptionParser()
|
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 unittest
|
||||||
import zipfile
|
import zipfile
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import tempfile
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
|
||||||
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())), '..'))
|
||||||
@ -19,6 +21,8 @@ if localmodule not in sys.path:
|
|||||||
import fdroidserver.common
|
import fdroidserver.common
|
||||||
import fdroidserver.index
|
import fdroidserver.index
|
||||||
import fdroidserver.signindex
|
import fdroidserver.signindex
|
||||||
|
import fdroidserver.publish
|
||||||
|
from testcommon import TmpCwd
|
||||||
|
|
||||||
|
|
||||||
GP_FINGERPRINT = 'B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135'
|
GP_FINGERPRINT = 'B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135'
|
||||||
@ -114,8 +118,117 @@ class IndexTest(unittest.TestCase):
|
|||||||
self.assertEqual(10, len(index['packages']))
|
self.assertEqual(10, len(index['packages']))
|
||||||
self.assertEqual('new_etag', new_etag)
|
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 __name__ == "__main__":
|
||||||
|
if os.path.basename(os.getcwd()) != 'tests' and os.path.isdir('tests'):
|
||||||
|
os.chdir('tests')
|
||||||
|
|
||||||
parser = optparse.OptionParser()
|
parser = optparse.OptionParser()
|
||||||
parser.add_option("-v", "--verbose", action="store_true", default=False,
|
parser.add_option("-v", "--verbose", action="store_true", default=False,
|
||||||
help="Spew out even more information than normal")
|
help="Spew out even more information than normal")
|
||||||
|
@ -12,6 +12,7 @@ icons_src:
|
|||||||
minSdkVersion: '4'
|
minSdkVersion: '4'
|
||||||
packageName: info.guardianproject.urzip
|
packageName: info.guardianproject.urzip
|
||||||
sig: e0ecb5fc2d63088e4a07ae410a127722
|
sig: e0ecb5fc2d63088e4a07ae410a127722
|
||||||
|
signer: 7eabd8c15de883d1e82b5df2fd4f7f769e498078e9ad6dc901f0e96db77ceac3
|
||||||
size: 9969
|
size: 9969
|
||||||
targetSdkVersion: '18'
|
targetSdkVersion: '18'
|
||||||
uses-permission: []
|
uses-permission: []
|
||||||
|
@ -24,6 +24,7 @@ nativecode:
|
|||||||
- x86_64
|
- x86_64
|
||||||
packageName: org.dyndns.fules.ck
|
packageName: org.dyndns.fules.ck
|
||||||
sig: 9bf7a6a67f95688daec75eab4b1436ac
|
sig: 9bf7a6a67f95688daec75eab4b1436ac
|
||||||
|
signer: 9326a2cc1a2f148202bc7837a0af3b81200bd37fd359c9e13a2296a71d342056
|
||||||
size: 132453
|
size: 132453
|
||||||
targetSdkVersion: '8'
|
targetSdkVersion: '8'
|
||||||
uses-permission:
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<fdroid>
|
<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>
|
<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>http://foobarfoobarfoobar.onion/fdroid/repo</mirror>
|
||||||
<mirror>https://foo.bar/fdroid/repo</mirror>
|
<mirror>https://foo.bar/fdroid/repo</mirror>
|
||||||
|
Loading…
Reference in New Issue
Block a user