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/>.
34 from binascii import hexlify, unhexlify
35 from datetime import datetime
36 from xml.dom.minidom import Document
40 from . import metadata
42 from . import signindex
43 from fdroidserver.common import FDroidPopen, FDroidPopenBytes, load_stats_fdroid_signing_key_fingerprints
44 from fdroidserver.exception import FDroidException, VerificationException, MetaDataException
47 def make(apps, sortedids, apks, repodir, archive):
48 """Generate the repo index files.
50 This requires properly initialized options and config objects.
52 :param apps: fully populated apps list
53 :param sortedids: app package IDs, sorted
54 :param apks: full populated apks list
55 :param repodir: the repo directory
56 :param archive: True if this is the archive repo, False if it's the
59 from fdroidserver.update import METADATA_VERSION
61 def _resolve_description_link(appid):
63 return "fdroid.app:" + appid, apps[appid].Name
64 raise MetaDataException("Cannot resolve app id " + appid)
66 if not common.options.nosign:
67 common.assert_config_keystore(common.config)
69 repodict = collections.OrderedDict()
70 repodict['timestamp'] = datetime.utcnow()
71 repodict['version'] = METADATA_VERSION
73 if common.config['repo_maxage'] != 0:
74 repodict['maxage'] = common.config['repo_maxage']
77 repodict['name'] = common.config['archive_name']
78 repodict['icon'] = os.path.basename(common.config['archive_icon'])
79 repodict['address'] = common.config['archive_url']
80 repodict['description'] = common.config['archive_description']
81 urlbasepath = os.path.basename(urllib.parse.urlparse(common.config['archive_url']).path)
83 repodict['name'] = common.config['repo_name']
84 repodict['icon'] = os.path.basename(common.config['repo_icon'])
85 repodict['address'] = common.config['repo_url']
86 repodict['description'] = common.config['repo_description']
87 urlbasepath = os.path.basename(urllib.parse.urlparse(common.config['repo_url']).path)
89 mirrorcheckfailed = False
91 for mirror in sorted(common.config.get('mirrors', [])):
92 base = os.path.basename(urllib.parse.urlparse(mirror).path.rstrip('/'))
93 if common.config.get('nonstandardwebroot') is not True and base != 'fdroid':
94 logging.error(_("mirror '%s' does not end with 'fdroid'!") % mirror)
95 mirrorcheckfailed = True
96 # must end with / or urljoin strips a whole path segment
97 if mirror.endswith('/'):
98 mirrors.append(urllib.parse.urljoin(mirror, urlbasepath))
100 mirrors.append(urllib.parse.urljoin(mirror + '/', urlbasepath))
101 for mirror in common.config.get('servergitmirrors', []):
102 for url in get_mirror_service_urls(mirror):
103 mirrors.append(url + '/' + repodir)
104 if mirrorcheckfailed:
105 raise FDroidException(_("Malformed repository mirrors."))
107 repodict['mirrors'] = mirrors
109 appsWithPackages = collections.OrderedDict()
110 for packageName in sortedids:
111 app = apps[packageName]
115 # only include apps with packages
117 if apk['packageName'] == packageName:
118 newapp = copy.copy(app) # update wiki needs unmodified description
119 newapp['Description'] = metadata.description_html(app['Description'],
120 _resolve_description_link)
121 appsWithPackages[packageName] = newapp
124 requestsdict = collections.OrderedDict()
125 for command in ('install', 'uninstall'):
127 key = command + '_list'
128 if key in common.config:
129 if isinstance(common.config[key], str):
130 packageNames = [common.config[key]]
131 elif all(isinstance(item, str) for item in common.config[key]):
132 packageNames = common.config[key]
134 raise TypeError(_('only accepts strings, lists, and tuples'))
135 requestsdict[command] = packageNames
137 fdroid_signing_key_fingerprints = load_stats_fdroid_signing_key_fingerprints()
139 make_v0(appsWithPackages, apks, repodir, repodict, requestsdict,
140 fdroid_signing_key_fingerprints)
141 make_v1(appsWithPackages, apks, repodir, repodict, requestsdict,
142 fdroid_signing_key_fingerprints)
145 def make_v1(apps, packages, repodir, repodict, requestsdict, fdroid_signing_key_fingerprints):
147 def _index_encoder_default(obj):
148 if isinstance(obj, set):
149 return sorted(list(obj))
150 if isinstance(obj, datetime):
151 # Java prefers milliseconds
152 # we also need to accound for time zone/daylight saving time
153 return int(calendar.timegm(obj.timetuple()) * 1000)
154 if isinstance(obj, dict):
155 d = collections.OrderedDict()
156 for key in sorted(obj.keys()):
159 raise TypeError(repr(obj) + " is not JSON serializable")
161 output = collections.OrderedDict()
162 output['repo'] = repodict
163 output['requests'] = requestsdict
165 # establish sort order of the index
166 v1_sort_packages(packages, repodir, fdroid_signing_key_fingerprints)
169 output['apps'] = appslist
170 for packageName, appdict in apps.items():
171 d = collections.OrderedDict()
173 for k, v in sorted(appdict.items()):
176 if k in ('builds', 'comments', 'metadatapath',
177 'ArchivePolicy', 'AutoUpdateMode', 'MaintainerNotes',
178 'Provides', 'Repo', 'RepoType', 'RequiresRoot',
179 'UpdateCheckData', 'UpdateCheckIgnore', 'UpdateCheckMode',
180 'UpdateCheckName', 'NoSourceSince', 'VercodeOperation'):
183 # name things after the App class fields in fdroidclient
186 elif k == 'CurrentVersionCode': # TODO make SuggestedVersionCode the canonical name
187 k = 'suggestedVersionCode'
188 elif k == 'CurrentVersion': # TODO make SuggestedVersionName the canonical name
189 k = 'suggestedVersionName'
190 elif k == 'AutoName':
191 if 'Name' not in apps[packageName]:
195 k = k[:1].lower() + k[1:]
198 # establish sort order in localized dicts
199 for app in output['apps']:
200 localized = app.get('localized')
202 lordered = collections.OrderedDict()
203 for lkey, lvalue in sorted(localized.items()):
204 lordered[lkey] = collections.OrderedDict()
205 for ikey, iname in sorted(lvalue.items()):
206 lordered[lkey][ikey] = iname
207 app['localized'] = lordered
209 output_packages = collections.OrderedDict()
210 output['packages'] = output_packages
211 for package in packages:
212 packageName = package['packageName']
213 if packageName not in apps:
214 logging.info(_('Ignoring package without metadata: ') + package['apkName'])
216 if packageName in output_packages:
217 packagelist = output_packages[packageName]
220 output_packages[packageName] = packagelist
221 d = collections.OrderedDict()
222 packagelist.append(d)
223 for k, v in sorted(package.items()):
226 if k in ('icon', 'icons', 'icons_src', 'name', ):
230 json_name = 'index-v1.json'
231 index_file = os.path.join(repodir, json_name)
232 with open(index_file, 'w') as fp:
233 if common.options.pretty:
234 json.dump(output, fp, default=_index_encoder_default, indent=2)
236 json.dump(output, fp, default=_index_encoder_default)
238 if common.options.nosign:
239 logging.debug(_('index-v1 must have a signature, use `fdroid signindex` to create it!'))
241 signindex.config = common.config
242 signindex.sign_index_v1(repodir, json_name)
245 def v1_sort_packages(packages, repodir, fdroid_signing_key_fingerprints):
246 """Sorts the supplied list to ensure a deterministic sort order for
247 package entries in the index file. This sort-order also expresses
248 installation preference to the clients.
249 (First in this list = first to install)
251 :param packages: list of packages which need to be sorted before but into index file.
255 GROUP_FDROID_SIGNED = 2
256 GROUP_OTHER_SIGNED = 3
258 def v1_sort_keys(package):
259 packageName = package.get('packageName', None)
261 sig = package.get('signer', None)
263 dev_sig = common.metadata_find_developer_signature(packageName)
264 group = GROUP_OTHER_SIGNED
265 if dev_sig and dev_sig == sig:
266 group = GROUP_DEV_SIGNED
268 fdroidsig = fdroid_signing_key_fingerprints.get(packageName, {}).get('signer')
269 if fdroidsig and fdroidsig == sig:
270 group = GROUP_FDROID_SIGNED
273 if package.get('versionCode', None):
274 versionCode = -int(package['versionCode'])
276 return(packageName, group, sig, versionCode)
278 packages.sort(key=v1_sort_keys)
281 def make_v0(apps, apks, repodir, repodict, requestsdict, fdroid_signing_key_fingerprints):
283 aka index.jar aka index.xml
288 def addElement(name, value, doc, parent):
289 el = doc.createElement(name)
290 el.appendChild(doc.createTextNode(value))
291 parent.appendChild(el)
293 def addElementNonEmpty(name, value, doc, parent):
296 addElement(name, value, doc, parent)
298 def addElementIfInApk(name, apk, key, doc, parent):
301 value = str(apk[key])
302 addElement(name, value, doc, parent)
304 def addElementCDATA(name, value, doc, parent):
305 el = doc.createElement(name)
306 el.appendChild(doc.createCDATASection(value))
307 parent.appendChild(el)
309 def addElementCheckLocalized(name, app, key, doc, parent, default=''):
310 '''Fill in field from metadata or localized block
312 For name/summary/description, they can come only from the app source,
313 or from a dir in fdroiddata. They can be entirely missing from the
314 metadata file if there is localized versions. This will fetch those
315 from the localized version if its not available in the metadata file.
318 el = doc.createElement(name)
320 lkey = key[:1].lower() + key[1:]
321 localized = app.get('localized')
322 if not value and localized:
323 for lang in ['en-US'] + [x for x in localized.keys()]:
324 if not lang.startswith('en'):
326 if lang in localized:
327 value = localized[lang].get(lkey)
330 if not value and localized and len(localized) > 1:
331 lang = list(localized.keys())[0]
332 value = localized[lang].get(lkey)
335 el.appendChild(doc.createTextNode(value))
336 parent.appendChild(el)
338 root = doc.createElement("fdroid")
339 doc.appendChild(root)
341 repoel = doc.createElement("repo")
343 repoel.setAttribute("name", repodict['name'])
344 if 'maxage' in repodict:
345 repoel.setAttribute("maxage", str(repodict['maxage']))
346 repoel.setAttribute("icon", os.path.basename(repodict['icon']))
347 repoel.setAttribute("url", repodict['address'])
348 addElement('description', repodict['description'], doc, repoel)
349 for mirror in repodict.get('mirrors', []):
350 addElement('mirror', mirror, doc, repoel)
352 repoel.setAttribute("version", str(repodict['version']))
353 repoel.setAttribute("timestamp", '%d' % repodict['timestamp'].timestamp())
355 pubkey, repo_pubkey_fingerprint = extract_pubkey()
356 repoel.setAttribute("pubkey", pubkey.decode('utf-8'))
357 root.appendChild(repoel)
359 for command in ('install', 'uninstall'):
360 for packageName in requestsdict[command]:
361 element = doc.createElement(command)
362 root.appendChild(element)
363 element.setAttribute('packageName', packageName)
365 for appid, appdict in apps.items():
366 app = metadata.App(appdict)
368 if app.Disabled is not None:
371 # Get a list of the apks for this app...
373 apksbyversion = collections.defaultdict(lambda: [])
375 if apk.get('versionCode') and apk.get('packageName') == appid:
376 apksbyversion[apk['versionCode']].append(apk)
377 for versionCode, apksforver in apksbyversion.items():
378 fdroidsig = fdroid_signing_key_fingerprints.get(appid, {}).get('signer')
379 fdroid_signed_apk = None
380 name_match_apk = None
382 if fdroidsig and x.get('signer', None) == fdroidsig:
383 fdroid_signed_apk = x
384 if common.apk_release_filename.match(x.get('apkName', '')):
386 # choose which of the available versions is most
387 # suiteable for index v0
388 if fdroid_signed_apk:
389 apklist.append(fdroid_signed_apk)
391 apklist.append(name_match_apk)
393 apklist.append(apksforver[0])
395 if len(apklist) == 0:
398 apel = doc.createElement("application")
399 apel.setAttribute("id", app.id)
400 root.appendChild(apel)
402 addElement('id', app.id, doc, apel)
404 addElement('added', app.added.strftime('%Y-%m-%d'), doc, apel)
406 addElement('lastupdated', app.lastUpdated.strftime('%Y-%m-%d'), doc, apel)
408 addElementCheckLocalized('name', app, 'Name', doc, apel)
409 addElementCheckLocalized('summary', app, 'Summary', doc, apel)
412 addElement('icon', app.icon, doc, apel)
414 addElementCheckLocalized('desc', app, 'Description', doc, apel,
415 '<p>No description available</p>')
417 addElement('license', app.License, doc, apel)
419 addElement('categories', ','.join(app.Categories), doc, apel)
420 # We put the first (primary) category in LAST, which will have
421 # the desired effect of making clients that only understand one
422 # category see that one.
423 addElement('category', app.Categories[0], doc, apel)
424 addElement('web', app.WebSite, doc, apel)
425 addElement('source', app.SourceCode, doc, apel)
426 addElement('tracker', app.IssueTracker, doc, apel)
427 addElementNonEmpty('changelog', app.Changelog, doc, apel)
428 addElementNonEmpty('author', app.AuthorName, doc, apel)
429 addElementNonEmpty('email', app.AuthorEmail, doc, apel)
430 addElementNonEmpty('donate', app.Donate, doc, apel)
431 addElementNonEmpty('bitcoin', app.Bitcoin, doc, apel)
432 addElementNonEmpty('litecoin', app.Litecoin, doc, apel)
433 addElementNonEmpty('flattr', app.FlattrID, doc, apel)
435 # These elements actually refer to the current version (i.e. which
436 # one is recommended. They are historically mis-named, and need
437 # changing, but stay like this for now to support existing clients.
438 addElement('marketversion', app.CurrentVersion, doc, apel)
439 addElement('marketvercode', app.CurrentVersionCode, doc, apel)
442 pv = app.Provides.split(',')
443 addElementNonEmpty('provides', ','.join(pv), doc, apel)
445 addElement('requirements', 'root', doc, apel)
447 # Sort the apk list into version order, just so the web site
448 # doesn't have to do any work by default...
449 apklist = sorted(apklist, key=lambda apk: apk['versionCode'], reverse=True)
451 if 'antiFeatures' in apklist[0]:
452 app.AntiFeatures.extend(apklist[0]['antiFeatures'])
454 addElementNonEmpty('antifeatures', ','.join(app.AntiFeatures), doc, apel)
456 # Check for duplicates - they will make the client unhappy...
457 for i in range(len(apklist) - 1):
459 second = apklist[i + 1]
460 if first['versionCode'] == second['versionCode'] \
461 and first['sig'] == second['sig']:
462 if first['hash'] == second['hash']:
463 raise FDroidException('"{0}/{1}" and "{0}/{2}" are exact duplicates!'.format(
464 repodir, first['apkName'], second['apkName']))
466 raise FDroidException('duplicates: "{0}/{1}" - "{0}/{2}"'.format(
467 repodir, first['apkName'], second['apkName']))
469 current_version_code = 0
470 current_version_file = None
472 file_extension = common.get_file_extension(apk['apkName'])
473 # find the APK for the "Current Version"
474 if current_version_code < apk['versionCode']:
475 current_version_code = apk['versionCode']
476 if current_version_code < int(app.CurrentVersionCode):
477 current_version_file = apk['apkName']
479 apkel = doc.createElement("package")
480 apel.appendChild(apkel)
481 addElement('version', apk['versionName'], doc, apkel)
482 addElement('versioncode', str(apk['versionCode']), doc, apkel)
483 addElement('apkname', apk['apkName'], doc, apkel)
484 addElementIfInApk('srcname', apk, 'srcname', doc, apkel)
486 hashel = doc.createElement("hash")
487 hashel.setAttribute('type', 'sha256')
488 hashel.appendChild(doc.createTextNode(apk['hash']))
489 apkel.appendChild(hashel)
491 addElement('size', str(apk['size']), doc, apkel)
492 addElementIfInApk('sdkver', apk,
493 'minSdkVersion', doc, apkel)
494 addElementIfInApk('targetSdkVersion', apk,
495 'targetSdkVersion', doc, apkel)
496 addElementIfInApk('maxsdkver', apk,
497 'maxSdkVersion', doc, apkel)
498 addElementIfInApk('obbMainFile', apk,
499 'obbMainFile', doc, apkel)
500 addElementIfInApk('obbMainFileSha256', apk,
501 'obbMainFileSha256', doc, apkel)
502 addElementIfInApk('obbPatchFile', apk,
503 'obbPatchFile', doc, apkel)
504 addElementIfInApk('obbPatchFileSha256', apk,
505 'obbPatchFileSha256', doc, apkel)
507 addElement('added', apk['added'].strftime('%Y-%m-%d'), doc, apkel)
509 if file_extension == 'apk': # sig is required for APKs, but only APKs
510 addElement('sig', apk['sig'], doc, apkel)
512 old_permissions = set()
513 sorted_permissions = sorted(apk['uses-permission'])
514 for perm in sorted_permissions:
515 perm_name = perm.name
516 if perm_name.startswith("android.permission."):
517 perm_name = perm_name[19:]
518 old_permissions.add(perm_name)
519 addElementNonEmpty('permissions', ','.join(sorted(old_permissions)), doc, apkel)
521 for permission in sorted_permissions:
522 permel = doc.createElement('uses-permission')
523 permel.setAttribute('name', permission.name)
524 if permission.maxSdkVersion is not None:
525 permel.setAttribute('maxSdkVersion', '%d' % permission.maxSdkVersion)
526 apkel.appendChild(permel)
527 for permission_sdk_23 in sorted(apk['uses-permission-sdk-23']):
528 permel = doc.createElement('uses-permission-sdk-23')
529 permel.setAttribute('name', permission_sdk_23.name)
530 if permission_sdk_23.maxSdkVersion is not None:
531 permel.setAttribute('maxSdkVersion', '%d' % permission_sdk_23.maxSdkVersion)
532 apkel.appendChild(permel)
533 if 'nativecode' in apk:
534 addElement('nativecode', ','.join(sorted(apk['nativecode'])), doc, apkel)
535 addElementNonEmpty('features', ','.join(sorted(apk['features'])), doc, apkel)
537 if current_version_file is not None \
538 and common.config['make_current_version_link'] \
539 and repodir == 'repo': # only create these
540 namefield = common.config['current_version_name_source']
541 sanitized_name = re.sub(b'''[ '"&%?+=/]''', b'', app.get(namefield).encode('utf-8'))
542 apklinkname = sanitized_name + os.path.splitext(current_version_file)[1].encode('utf-8')
543 current_version_path = os.path.join(repodir, current_version_file).encode('utf-8', 'surrogateescape')
544 if os.path.islink(apklinkname):
545 os.remove(apklinkname)
546 os.symlink(current_version_path, apklinkname)
547 # also symlink gpg signature, if it exists
548 for extension in (b'.asc', b'.sig'):
549 sigfile_path = current_version_path + extension
550 if os.path.exists(sigfile_path):
551 siglinkname = apklinkname + extension
552 if os.path.islink(siglinkname):
553 os.remove(siglinkname)
554 os.symlink(sigfile_path, siglinkname)
556 if common.options.pretty:
557 output = doc.toprettyxml(encoding='utf-8')
559 output = doc.toxml(encoding='utf-8')
561 with open(os.path.join(repodir, 'index.xml'), 'wb') as f:
564 if 'repo_keyalias' in common.config:
566 if common.options.nosign:
567 logging.info(_("Creating unsigned index in preparation for signing"))
569 logging.info(_("Creating signed index with this key (SHA256):"))
570 logging.info("%s" % repo_pubkey_fingerprint)
572 # Create a jar of the index...
573 jar_output = 'index_unsigned.jar' if common.options.nosign else 'index.jar'
574 p = FDroidPopen(['jar', 'cf', jar_output, 'index.xml'], cwd=repodir)
575 if p.returncode != 0:
576 raise FDroidException("Failed to create {0}".format(jar_output))
579 signed = os.path.join(repodir, 'index.jar')
580 if common.options.nosign:
581 # Remove old signed index if not signing
582 if os.path.exists(signed):
585 signindex.config = common.config
586 signindex.sign_jar(signed)
588 # Copy the repo icon into the repo directory...
589 icon_dir = os.path.join(repodir, 'icons')
590 iconfilename = os.path.join(icon_dir, os.path.basename(common.config['repo_icon']))
591 shutil.copyfile(common.config['repo_icon'], iconfilename)
594 def extract_pubkey():
596 Extracts and returns the repository's public key from the keystore.
597 :return: public key in hex, repository fingerprint
599 if 'repo_pubkey' in common.config:
600 pubkey = unhexlify(common.config['repo_pubkey'])
602 env_vars = {'FDROID_KEY_STORE_PASS': common.config['keystorepass']}
603 p = FDroidPopenBytes([common.config['keytool'], '-exportcert',
604 '-alias', common.config['repo_keyalias'],
605 '-keystore', common.config['keystore'],
606 '-storepass:env', 'FDROID_KEY_STORE_PASS']
607 + common.config['smartcardoptions'],
608 envs=env_vars, output=False, stderr_to_stdout=False)
609 if p.returncode != 0 or len(p.output) < 20:
610 msg = "Failed to get repo pubkey!"
611 if common.config['keystore'] == 'NONE':
612 msg += ' Is your crypto smartcard plugged in?'
613 raise FDroidException(msg)
615 repo_pubkey_fingerprint = common.get_cert_fingerprint(pubkey)
616 return hexlify(pubkey), repo_pubkey_fingerprint
619 def get_mirror_service_urls(url):
620 '''Get direct URLs from git service for use by fdroidclient
622 Via 'servergitmirrors', fdroidserver can create and push a mirror
623 to certain well known git services like gitlab or github. This
624 will always use the 'master' branch since that is the default
625 branch in git. The files are then accessible via alternate URLs,
626 where they are served in their raw format via a CDN rather than
630 if url.startswith('git@'):
631 url = re.sub(r'^git@(.*):(.*)', r'https://\1/\2', url)
633 segments = url.split("/")
635 if segments[4].endswith('.git'):
636 segments[4] = segments[4][:-4]
638 hostname = segments[2]
645 if hostname == "github.com":
646 # Github-like RAW segments "https://raw.githubusercontent.com/user/repo/branch/folder"
647 segments[2] = "raw.githubusercontent.com"
648 segments.extend([branch, folder])
649 urls.append('/'.join(segments))
650 elif hostname == "gitlab.com":
651 # Gitlab Raw "https://gitlab.com/user/repo/raw/branch/folder"
652 gitlab_raw = segments + ['raw', branch, folder]
653 urls.append('/'.join(gitlab_raw))
654 # Gitlab-like Pages segments "https://user.gitlab.io/repo/folder"
655 gitlab_pages = ["https:", "", user + ".gitlab.io", repo, folder]
656 urls.append('/'.join(gitlab_pages))
662 def download_repo_index(url_str, etag=None, verify_fingerprint=True):
664 Downloads the repository index from the given :param url_str
665 and verifies the repository's fingerprint if :param verify_fingerprint is not False.
667 :raises: VerificationException() if the repository could not be verified
669 :return: A tuple consisting of:
670 - The index in JSON format or None if the index did not change
671 - The new eTag as returned by the HTTP request
673 url = urllib.parse.urlsplit(url_str)
676 if verify_fingerprint:
677 query = urllib.parse.parse_qs(url.query)
678 if 'fingerprint' not in query:
679 raise VerificationException(_("No fingerprint in URL."))
680 fingerprint = query['fingerprint'][0]
682 url = urllib.parse.SplitResult(url.scheme, url.netloc, url.path + '/index-v1.jar', '', '')
683 download, new_etag = net.http_get(url.geturl(), etag)
686 return None, new_etag
688 with tempfile.NamedTemporaryFile() as fp:
689 # write and open JAR file
691 jar = zipfile.ZipFile(fp)
693 # verify that the JAR signature is valid
694 common.verify_jar_signature(fp.name)
696 # get public key and its fingerprint from JAR
697 public_key, public_key_fingerprint = get_public_key_from_jar(jar)
699 # compare the fingerprint if verify_fingerprint is True
700 if verify_fingerprint and fingerprint.upper() != public_key_fingerprint:
701 raise VerificationException(_("The repository's fingerprint does not match."))
703 # load repository index from JSON
704 index = json.loads(jar.read('index-v1.json').decode("utf-8"))
705 index["repo"]["pubkey"] = hexlify(public_key).decode("utf-8")
706 index["repo"]["fingerprint"] = public_key_fingerprint
708 # turn the apps into App objects
709 index["apps"] = [metadata.App(app) for app in index["apps"]]
711 return index, new_etag
714 def get_public_key_from_jar(jar):
716 Get the public key and its fingerprint from a JAR file.
718 :raises: VerificationException() if the JAR was not signed exactly once
720 :param jar: a zipfile.ZipFile object
721 :return: the public key from the jar and its fingerprint
723 # extract certificate from jar
724 certs = [n for n in jar.namelist() if common.CERT_PATH_REGEX.match(n)]
726 raise VerificationException(_("Found no signing certificates for repository."))
728 raise VerificationException(_("Found multiple signing certificates for repository."))
730 # extract public key from certificate
731 public_key = common.get_certificate(jar.read(certs[0]))
732 public_key_fingerprint = common.get_cert_fingerprint(public_key).replace(' ', '')
734 return public_key, public_key_fingerprint