3 # update.py - part of the FDroid server tools
4 # Copyright (C) 2017, Torsten Grote <t at grobox dot de>
5 # Copyright (C) 2016, Blue Jay Wireless
6 # Copyright (C) 2014-2016, Hans-Christoph Steiner <hans@eds.org>
7 # Copyright (C) 2010-2015, Ciaran Gultnieks <ciaran@ciarang.com>
8 # Copyright (C) 2013-2014, Daniel Martà <mvdan@mvdan.cc>
10 # This program is free software: you can redistribute it and/or modify
11 # it under the terms of the GNU Affero General Public License as published by
12 # the Free Software Foundation, either version 3 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU Affero General Public License for more details.
20 # You should have received a copy of the GNU Affero General Public License
21 # along with this program. If not, see <http://www.gnu.org/licenses/>.
33 from binascii import hexlify, unhexlify
34 from datetime import datetime
35 from xml.dom.minidom import Document
39 from . import metadata
41 from . import signindex
42 from fdroidserver.common import FDroidPopen, FDroidPopenBytes, load_stats_fdroid_signing_key_fingerprints
43 from fdroidserver.exception import FDroidException, VerificationException, MetaDataException
46 def make(apps, sortedids, apks, repodir, archive):
47 """Generate the repo index files.
49 This requires properly initialized options and config objects.
51 :param apps: fully populated apps list
52 :param sortedids: app package IDs, sorted
53 :param apks: full populated apks list
54 :param repodir: the repo directory
55 :param archive: True if this is the archive repo, False if it's the
58 from fdroidserver.update import METADATA_VERSION
60 def _resolve_description_link(appid):
62 return "fdroid.app:" + appid, apps[appid].Name
63 raise MetaDataException("Cannot resolve app id " + appid)
66 if not common.options.nosign:
67 if 'repo_keyalias' not in common.config:
69 logging.critical(_("'repo_keyalias' not found in config.py!"))
70 if 'keystore' not in common.config:
72 logging.critical(_("'keystore' not found in config.py!"))
73 if 'keystorepass' not in common.config:
75 logging.critical(_("'keystorepass' not found in config.py!"))
76 if 'keypass' not in common.config:
78 logging.critical(_("'keypass' not found in config.py!"))
79 if not os.path.exists(common.config['keystore']):
81 logging.critical("'" + common.config['keystore'] + "' does not exist!")
83 raise FDroidException("`fdroid update` requires a signing key, " +
84 "you can create one using: fdroid update --create-key")
86 repodict = collections.OrderedDict()
87 repodict['timestamp'] = datetime.utcnow()
88 repodict['version'] = METADATA_VERSION
90 if common.config['repo_maxage'] != 0:
91 repodict['maxage'] = common.config['repo_maxage']
94 repodict['name'] = common.config['archive_name']
95 repodict['icon'] = os.path.basename(common.config['archive_icon'])
96 repodict['address'] = common.config['archive_url']
97 repodict['description'] = common.config['archive_description']
98 urlbasepath = os.path.basename(urllib.parse.urlparse(common.config['archive_url']).path)
100 repodict['name'] = common.config['repo_name']
101 repodict['icon'] = os.path.basename(common.config['repo_icon'])
102 repodict['address'] = common.config['repo_url']
103 repodict['description'] = common.config['repo_description']
104 urlbasepath = os.path.basename(urllib.parse.urlparse(common.config['repo_url']).path)
106 mirrorcheckfailed = False
108 for mirror in sorted(common.config.get('mirrors', [])):
109 base = os.path.basename(urllib.parse.urlparse(mirror).path.rstrip('/'))
110 if common.config.get('nonstandardwebroot') is not True and base != 'fdroid':
111 logging.error(_("mirror '%s' does not end with 'fdroid'!") % mirror)
112 mirrorcheckfailed = True
113 # must end with / or urljoin strips a whole path segment
114 if mirror.endswith('/'):
115 mirrors.append(urllib.parse.urljoin(mirror, urlbasepath))
117 mirrors.append(urllib.parse.urljoin(mirror + '/', urlbasepath))
118 for mirror in common.config.get('servergitmirrors', []):
119 for url in get_mirror_service_urls(mirror):
120 mirrors.append(url + '/' + repodir)
121 if mirrorcheckfailed:
122 raise FDroidException(_("Malformed repository mirrors."))
124 repodict['mirrors'] = mirrors
126 appsWithPackages = collections.OrderedDict()
127 for packageName in sortedids:
128 app = apps[packageName]
132 # only include apps with packages
134 if apk['packageName'] == packageName:
135 newapp = copy.copy(app) # update wiki needs unmodified description
136 newapp['Description'] = metadata.description_html(app['Description'],
137 _resolve_description_link)
138 appsWithPackages[packageName] = newapp
141 requestsdict = collections.OrderedDict()
142 for command in ('install', 'uninstall'):
144 key = command + '_list'
145 if key in common.config:
146 if isinstance(common.config[key], str):
147 packageNames = [common.config[key]]
148 elif all(isinstance(item, str) for item in common.config[key]):
149 packageNames = common.config[key]
151 raise TypeError(_('only accepts strings, lists, and tuples'))
152 requestsdict[command] = packageNames
154 fdroid_signing_key_fingerprints = load_stats_fdroid_signing_key_fingerprints()
156 make_v0(appsWithPackages, apks, repodir, repodict, requestsdict,
157 fdroid_signing_key_fingerprints)
158 make_v1(appsWithPackages, apks, repodir, repodict, requestsdict,
159 fdroid_signing_key_fingerprints)
162 def make_v1(apps, packages, repodir, repodict, requestsdict, fdroid_signing_key_fingerprints):
164 def _index_encoder_default(obj):
165 if isinstance(obj, set):
167 if isinstance(obj, datetime):
168 return int(obj.timestamp() * 1000) # Java expects milliseconds
169 raise TypeError(repr(obj) + " is not JSON serializable")
171 output = collections.OrderedDict()
172 output['repo'] = repodict
173 output['requests'] = requestsdict
175 # establish sort order of the index
176 v1_sort_packages(packages, repodir, fdroid_signing_key_fingerprints)
179 output['apps'] = appslist
180 for packageName, appdict in apps.items():
181 d = collections.OrderedDict()
183 for k, v in sorted(appdict.items()):
186 if k in ('builds', 'comments', 'metadatapath',
187 'ArchivePolicy', 'AutoUpdateMode', 'MaintainerNotes',
188 'Provides', 'Repo', 'RepoType', 'RequiresRoot',
189 'UpdateCheckData', 'UpdateCheckIgnore', 'UpdateCheckMode',
190 'UpdateCheckName', 'NoSourceSince', 'VercodeOperation'):
193 # name things after the App class fields in fdroidclient
196 elif k == 'CurrentVersionCode': # TODO make SuggestedVersionCode the canonical name
197 k = 'suggestedVersionCode'
198 elif k == 'CurrentVersion': # TODO make SuggestedVersionName the canonical name
199 k = 'suggestedVersionName'
200 elif k == 'AutoName':
201 if 'Name' not in apps[packageName]:
205 k = k[:1].lower() + k[1:]
208 output_packages = collections.OrderedDict()
209 output['packages'] = output_packages
210 for package in packages:
211 packageName = package['packageName']
212 if packageName not in apps:
213 logging.info(_('Ignoring package without metadata: ') + package['apkName'])
215 if packageName in output_packages:
216 packagelist = output_packages[packageName]
219 output_packages[packageName] = packagelist
220 d = collections.OrderedDict()
221 packagelist.append(d)
222 for k, v in sorted(package.items()):
225 if k in ('icon', 'icons', 'icons_src', 'name', ):
229 json_name = 'index-v1.json'
230 index_file = os.path.join(repodir, json_name)
231 with open(index_file, 'w') as fp:
232 if common.options.pretty:
233 json.dump(output, fp, default=_index_encoder_default, indent=2)
235 json.dump(output, fp, default=_index_encoder_default)
237 if common.options.nosign:
238 logging.debug(_('index-v1 must have a signature, use `fdroid signindex` to create it!'))
240 signindex.config = common.config
241 signindex.sign_index_v1(repodir, json_name)
244 def v1_sort_packages(packages, repodir, fdroid_signing_key_fingerprints):
245 """Sorts the supplied list to ensure a deterministic sort order for
246 package entries in the index file. This sort-order also expresses
247 installation preference to the clients.
248 (First in this list = first to install)
250 :param packages: list of packages which need to be sorted before but into index file.
254 GROUP_FDROID_SIGNED = 2
255 GROUP_OTHER_SIGNED = 3
257 def v1_sort_keys(package):
258 packageName = package.get('packageName', None)
260 sig = package.get('signer', None)
262 dev_sig = common.metadata_find_developer_signature(packageName)
263 group = GROUP_OTHER_SIGNED
264 if dev_sig and dev_sig == sig:
265 group = GROUP_DEV_SIGNED
267 fdroidsig = fdroid_signing_key_fingerprints.get(packageName, {}).get('signer')
268 if fdroidsig and fdroidsig == sig:
269 group = GROUP_FDROID_SIGNED
272 if package.get('versionCode', None):
273 versionCode = -int(package['versionCode'])
275 return(packageName, group, sig, versionCode)
277 packages.sort(key=v1_sort_keys)
280 def make_v0(apps, apks, repodir, repodict, requestsdict, fdroid_signing_key_fingerprints):
282 aka index.jar aka index.xml
287 def addElement(name, value, doc, parent):
288 el = doc.createElement(name)
289 el.appendChild(doc.createTextNode(value))
290 parent.appendChild(el)
292 def addElementNonEmpty(name, value, doc, parent):
295 addElement(name, value, doc, parent)
297 def addElementIfInApk(name, apk, key, doc, parent):
300 value = str(apk[key])
301 addElement(name, value, doc, parent)
303 def addElementCDATA(name, value, doc, parent):
304 el = doc.createElement(name)
305 el.appendChild(doc.createCDATASection(value))
306 parent.appendChild(el)
308 def addElementCheckLocalized(name, app, key, doc, parent, default=''):
309 '''Fill in field from metadata or localized block
311 For name/summary/description, they can come only from the app source,
312 or from a dir in fdroiddata. They can be entirely missing from the
313 metadata file if there is localized versions. This will fetch those
314 from the localized version if its not available in the metadata file.
317 el = doc.createElement(name)
319 lkey = key[:1].lower() + key[1:]
320 localized = app.get('localized')
321 if not value and localized:
322 for lang in ['en-US'] + [x for x in localized.keys()]:
323 if not lang.startswith('en'):
325 if lang in localized:
326 value = localized[lang].get(lkey)
329 if not value and localized and len(localized) > 1:
330 lang = list(localized.keys())[0]
331 value = localized[lang].get(lkey)
334 el.appendChild(doc.createTextNode(value))
335 parent.appendChild(el)
337 root = doc.createElement("fdroid")
338 doc.appendChild(root)
340 repoel = doc.createElement("repo")
342 repoel.setAttribute("name", repodict['name'])
343 if 'maxage' in repodict:
344 repoel.setAttribute("maxage", str(repodict['maxage']))
345 repoel.setAttribute("icon", os.path.basename(repodict['icon']))
346 repoel.setAttribute("url", repodict['address'])
347 addElement('description', repodict['description'], doc, repoel)
348 for mirror in repodict.get('mirrors', []):
349 addElement('mirror', mirror, doc, repoel)
351 repoel.setAttribute("version", str(repodict['version']))
352 repoel.setAttribute("timestamp", '%d' % repodict['timestamp'].timestamp())
354 pubkey, repo_pubkey_fingerprint = extract_pubkey()
355 repoel.setAttribute("pubkey", pubkey.decode('utf-8'))
356 root.appendChild(repoel)
358 for command in ('install', 'uninstall'):
359 for packageName in requestsdict[command]:
360 element = doc.createElement(command)
361 root.appendChild(element)
362 element.setAttribute('packageName', packageName)
364 for appid, appdict in apps.items():
365 app = metadata.App(appdict)
367 if app.Disabled is not None:
370 # Get a list of the apks for this app...
372 apksbyversion = collections.defaultdict(lambda: [])
374 if apk.get('versionCode') and apk.get('packageName') == appid:
375 apksbyversion[apk['versionCode']].append(apk)
376 for versionCode, apksforver in apksbyversion.items():
377 fdroidsig = fdroid_signing_key_fingerprints.get(appid, {}).get('signer')
378 fdroid_signed_apk = None
379 name_match_apk = None
381 if fdroidsig and x.get('signer', None) == fdroidsig:
382 fdroid_signed_apk = x
383 if common.apk_release_filename.match(x.get('apkName', '')):
385 # choose which of the available versions is most
386 # suiteable for index v0
387 if fdroid_signed_apk:
388 apklist.append(fdroid_signed_apk)
390 apklist.append(name_match_apk)
392 apklist.append(apksforver[0])
394 if len(apklist) == 0:
397 apel = doc.createElement("application")
398 apel.setAttribute("id", app.id)
399 root.appendChild(apel)
401 addElement('id', app.id, doc, apel)
403 addElement('added', app.added.strftime('%Y-%m-%d'), doc, apel)
405 addElement('lastupdated', app.lastUpdated.strftime('%Y-%m-%d'), doc, apel)
407 addElementCheckLocalized('name', app, 'Name', doc, apel)
408 addElementCheckLocalized('summary', app, 'Summary', doc, apel)
411 addElement('icon', app.icon, doc, apel)
413 addElementCheckLocalized('desc', app, 'Description', doc, apel,
414 '<p>No description available</p>')
416 addElement('license', app.License, doc, apel)
418 addElement('categories', ','.join(app.Categories), doc, apel)
419 # We put the first (primary) category in LAST, which will have
420 # the desired effect of making clients that only understand one
421 # category see that one.
422 addElement('category', app.Categories[0], doc, apel)
423 addElement('web', app.WebSite, doc, apel)
424 addElement('source', app.SourceCode, doc, apel)
425 addElement('tracker', app.IssueTracker, doc, apel)
426 addElementNonEmpty('changelog', app.Changelog, doc, apel)
427 addElementNonEmpty('author', app.AuthorName, doc, apel)
428 addElementNonEmpty('email', app.AuthorEmail, doc, apel)
429 addElementNonEmpty('donate', app.Donate, doc, apel)
430 addElementNonEmpty('bitcoin', app.Bitcoin, doc, apel)
431 addElementNonEmpty('litecoin', app.Litecoin, doc, apel)
432 addElementNonEmpty('flattr', app.FlattrID, doc, apel)
434 # These elements actually refer to the current version (i.e. which
435 # one is recommended. They are historically mis-named, and need
436 # changing, but stay like this for now to support existing clients.
437 addElement('marketversion', app.CurrentVersion, doc, apel)
438 addElement('marketvercode', app.CurrentVersionCode, doc, apel)
441 pv = app.Provides.split(',')
442 addElementNonEmpty('provides', ','.join(pv), doc, apel)
444 addElement('requirements', 'root', doc, apel)
446 # Sort the apk list into version order, just so the web site
447 # doesn't have to do any work by default...
448 apklist = sorted(apklist, key=lambda apk: apk['versionCode'], reverse=True)
450 if 'antiFeatures' in apklist[0]:
451 app.AntiFeatures.extend(apklist[0]['antiFeatures'])
453 addElementNonEmpty('antifeatures', ','.join(app.AntiFeatures), doc, apel)
455 # Check for duplicates - they will make the client unhappy...
456 for i in range(len(apklist) - 1):
458 second = apklist[i + 1]
459 if first['versionCode'] == second['versionCode'] \
460 and first['sig'] == second['sig']:
461 if first['hash'] == second['hash']:
462 raise FDroidException('"{0}/{1}" and "{0}/{2}" are exact duplicates!'.format(
463 repodir, first['apkName'], second['apkName']))
465 raise FDroidException('duplicates: "{0}/{1}" - "{0}/{2}"'.format(
466 repodir, first['apkName'], second['apkName']))
468 current_version_code = 0
469 current_version_file = None
471 file_extension = common.get_file_extension(apk['apkName'])
472 # find the APK for the "Current Version"
473 if current_version_code < apk['versionCode']:
474 current_version_code = apk['versionCode']
475 if current_version_code < int(app.CurrentVersionCode):
476 current_version_file = apk['apkName']
478 apkel = doc.createElement("package")
479 apel.appendChild(apkel)
480 addElement('version', apk['versionName'], doc, apkel)
481 addElement('versioncode', str(apk['versionCode']), doc, apkel)
482 addElement('apkname', apk['apkName'], doc, apkel)
483 addElementIfInApk('srcname', apk, 'srcname', doc, apkel)
485 hashel = doc.createElement("hash")
486 hashel.setAttribute('type', 'sha256')
487 hashel.appendChild(doc.createTextNode(apk['hash']))
488 apkel.appendChild(hashel)
490 addElement('size', str(apk['size']), doc, apkel)
491 addElementIfInApk('sdkver', apk,
492 'minSdkVersion', doc, apkel)
493 addElementIfInApk('targetSdkVersion', apk,
494 'targetSdkVersion', doc, apkel)
495 addElementIfInApk('maxsdkver', apk,
496 'maxSdkVersion', doc, apkel)
497 addElementIfInApk('obbMainFile', apk,
498 'obbMainFile', doc, apkel)
499 addElementIfInApk('obbMainFileSha256', apk,
500 'obbMainFileSha256', doc, apkel)
501 addElementIfInApk('obbPatchFile', apk,
502 'obbPatchFile', doc, apkel)
503 addElementIfInApk('obbPatchFileSha256', apk,
504 'obbPatchFileSha256', doc, apkel)
506 addElement('added', apk['added'].strftime('%Y-%m-%d'), doc, apkel)
508 if file_extension == 'apk': # sig is required for APKs, but only APKs
509 addElement('sig', apk['sig'], doc, apkel)
511 old_permissions = set()
512 sorted_permissions = sorted(apk['uses-permission'])
513 for perm in sorted_permissions:
514 perm_name = perm.name
515 if perm_name.startswith("android.permission."):
516 perm_name = perm_name[19:]
517 old_permissions.add(perm_name)
518 addElementNonEmpty('permissions', ','.join(sorted(old_permissions)), doc, apkel)
520 for permission in sorted_permissions:
521 permel = doc.createElement('uses-permission')
522 permel.setAttribute('name', permission.name)
523 if permission.maxSdkVersion is not None:
524 permel.setAttribute('maxSdkVersion', '%d' % permission.maxSdkVersion)
525 apkel.appendChild(permel)
526 for permission_sdk_23 in sorted(apk['uses-permission-sdk-23']):
527 permel = doc.createElement('uses-permission-sdk-23')
528 permel.setAttribute('name', permission_sdk_23.name)
529 if permission_sdk_23.maxSdkVersion is not None:
530 permel.setAttribute('maxSdkVersion', '%d' % permission_sdk_23.maxSdkVersion)
531 apkel.appendChild(permel)
532 if 'nativecode' in apk:
533 addElement('nativecode', ','.join(sorted(apk['nativecode'])), doc, apkel)
534 addElementNonEmpty('features', ','.join(sorted(apk['features'])), doc, apkel)
536 if current_version_file is not None \
537 and common.config['make_current_version_link'] \
538 and repodir == 'repo': # only create these
539 namefield = common.config['current_version_name_source']
540 sanitized_name = re.sub(b'''[ '"&%?+=/]''', b'', app.get(namefield).encode('utf-8'))
541 apklinkname = sanitized_name + os.path.splitext(current_version_file)[1].encode('utf-8')
542 current_version_path = os.path.join(repodir, current_version_file).encode('utf-8', 'surrogateescape')
543 if os.path.islink(apklinkname):
544 os.remove(apklinkname)
545 os.symlink(current_version_path, apklinkname)
546 # also symlink gpg signature, if it exists
547 for extension in (b'.asc', b'.sig'):
548 sigfile_path = current_version_path + extension
549 if os.path.exists(sigfile_path):
550 siglinkname = apklinkname + extension
551 if os.path.islink(siglinkname):
552 os.remove(siglinkname)
553 os.symlink(sigfile_path, siglinkname)
555 if common.options.pretty:
556 output = doc.toprettyxml(encoding='utf-8')
558 output = doc.toxml(encoding='utf-8')
560 with open(os.path.join(repodir, 'index.xml'), 'wb') as f:
563 if 'repo_keyalias' in common.config:
565 if common.options.nosign:
566 logging.info(_("Creating unsigned index in preparation for signing"))
568 logging.info(_("Creating signed index with this key (SHA256):"))
569 logging.info("%s" % repo_pubkey_fingerprint)
571 # Create a jar of the index...
572 jar_output = 'index_unsigned.jar' if common.options.nosign else 'index.jar'
573 p = FDroidPopen(['jar', 'cf', jar_output, 'index.xml'], cwd=repodir)
574 if p.returncode != 0:
575 raise FDroidException("Failed to create {0}".format(jar_output))
578 signed = os.path.join(repodir, 'index.jar')
579 if common.options.nosign:
580 # Remove old signed index if not signing
581 if os.path.exists(signed):
584 signindex.config = common.config
585 signindex.sign_jar(signed)
587 # Copy the repo icon into the repo directory...
588 icon_dir = os.path.join(repodir, 'icons')
589 iconfilename = os.path.join(icon_dir, os.path.basename(common.config['repo_icon']))
590 shutil.copyfile(common.config['repo_icon'], iconfilename)
593 def extract_pubkey():
595 Extracts and returns the repository's public key from the keystore.
596 :return: public key in hex, repository fingerprint
598 if 'repo_pubkey' in common.config:
599 pubkey = unhexlify(common.config['repo_pubkey'])
601 env_vars = {'FDROID_KEY_STORE_PASS': common.config['keystorepass']}
602 p = FDroidPopenBytes([common.config['keytool'], '-exportcert',
603 '-alias', common.config['repo_keyalias'],
604 '-keystore', common.config['keystore'],
605 '-storepass:env', 'FDROID_KEY_STORE_PASS']
606 + common.config['smartcardoptions'],
607 envs=env_vars, output=False, stderr_to_stdout=False)
608 if p.returncode != 0 or len(p.output) < 20:
609 msg = "Failed to get repo pubkey!"
610 if common.config['keystore'] == 'NONE':
611 msg += ' Is your crypto smartcard plugged in?'
612 raise FDroidException(msg)
614 repo_pubkey_fingerprint = common.get_cert_fingerprint(pubkey)
615 return hexlify(pubkey), repo_pubkey_fingerprint
618 def get_mirror_service_urls(url):
619 '''Get direct URLs from git service for use by fdroidclient
621 Via 'servergitmirrors', fdroidserver can create and push a mirror
622 to certain well known git services like gitlab or github. This
623 will always use the 'master' branch since that is the default
624 branch in git. The files are then accessible via alternate URLs,
625 where they are served in their raw format via a CDN rather than
629 if url.startswith('git@'):
630 url = re.sub(r'^git@(.*):(.*)', r'https://\1/\2', url)
632 segments = url.split("/")
634 if segments[4].endswith('.git'):
635 segments[4] = segments[4][:-4]
637 hostname = segments[2]
644 if hostname == "github.com":
645 # Github-like RAW segments "https://raw.githubusercontent.com/user/repo/branch/folder"
646 segments[2] = "raw.githubusercontent.com"
647 segments.extend([branch, folder])
648 urls.append('/'.join(segments))
649 elif hostname == "gitlab.com":
650 # Gitlab Raw "https://gitlab.com/user/repo/raw/branch/folder"
651 gitlab_raw = segments + ['raw', branch, folder]
652 urls.append('/'.join(gitlab_raw))
653 # Gitlab-like Pages segments "https://user.gitlab.io/repo/folder"
654 gitlab_pages = ["https:", "", user + ".gitlab.io", repo, folder]
655 urls.append('/'.join(gitlab_pages))
661 def download_repo_index(url_str, etag=None, verify_fingerprint=True):
663 Downloads the repository index from the given :param url_str
664 and verifies the repository's fingerprint if :param verify_fingerprint is not False.
666 :raises: VerificationException() if the repository could not be verified
668 :return: A tuple consisting of:
669 - The index in JSON format or None if the index did not change
670 - The new eTag as returned by the HTTP request
672 url = urllib.parse.urlsplit(url_str)
675 if verify_fingerprint:
676 query = urllib.parse.parse_qs(url.query)
677 if 'fingerprint' not in query:
678 raise VerificationException(_("No fingerprint in URL."))
679 fingerprint = query['fingerprint'][0]
681 url = urllib.parse.SplitResult(url.scheme, url.netloc, url.path + '/index-v1.jar', '', '')
682 download, new_etag = net.http_get(url.geturl(), etag)
685 return None, new_etag
687 with tempfile.NamedTemporaryFile() as fp:
688 # write and open JAR file
690 jar = zipfile.ZipFile(fp)
692 # verify that the JAR signature is valid
693 common.verify_jar_signature(fp.name)
695 # get public key and its fingerprint from JAR
696 public_key, public_key_fingerprint = get_public_key_from_jar(jar)
698 # compare the fingerprint if verify_fingerprint is True
699 if verify_fingerprint and fingerprint.upper() != public_key_fingerprint:
700 raise VerificationException(_("The repository's fingerprint does not match."))
702 # load repository index from JSON
703 index = json.loads(jar.read('index-v1.json').decode("utf-8"))
704 index["repo"]["pubkey"] = hexlify(public_key).decode("utf-8")
705 index["repo"]["fingerprint"] = public_key_fingerprint
707 # turn the apps into App objects
708 index["apps"] = [metadata.App(app) for app in index["apps"]]
710 return index, new_etag
713 def get_public_key_from_jar(jar):
715 Get the public key and its fingerprint from a JAR file.
717 :raises: VerificationException() if the JAR was not signed exactly once
719 :param jar: a zipfile.ZipFile object
720 :return: the public key from the jar and its fingerprint
722 # extract certificate from jar
723 certs = [n for n in jar.namelist() if common.CERT_PATH_REGEX.match(n)]
725 raise VerificationException(_("Found no signing certificates for repository."))
727 raise VerificationException(_("Found multiple signing certificates for repository."))
729 # extract public key from certificate
730 public_key = common.get_certificate(jar.read(certs[0]))
731 public_key_fingerprint = common.get_cert_fingerprint(public_key).replace(' ', '')
733 return public_key, public_key_fingerprint