chiark / gitweb /
Eliminate the need for password files
[fdroidserver.git] / fdroidserver / index.py
1 #!/usr/bin/env python3
2 #
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>
9 #
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.
14 #
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.
19 #
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/>.
22
23 import collections
24 import copy
25 import json
26 import logging
27 import os
28 import re
29 import shutil
30 import sys
31 import tempfile
32 import urllib.parse
33 import zipfile
34 from binascii import hexlify, unhexlify
35 from datetime import datetime
36 from xml.dom.minidom import Document
37
38 import requests
39
40 from fdroidserver import metadata, signindex, common
41 from fdroidserver.common import FDroidPopen, FDroidPopenBytes
42 from fdroidserver.metadata import MetaDataException
43
44
45 def make(apps, sortedids, apks, repodir, archive):
46     """Generate the repo index files.
47
48     This requires properly initialized options and config objects.
49
50     :param apps: fully populated apps list
51     :param sortedids: app package IDs, sorted
52     :param apks: full populated apks list
53     :param repodir: the repo directory
54     :param archive: True if this is the archive repo, False if it's the
55                     main one.
56     """
57     from fdroidserver.update import METADATA_VERSION
58
59     def _resolve_description_link(appid):
60         if appid in apps:
61             return "fdroid.app:" + appid, apps[appid].Name
62         raise MetaDataException("Cannot resolve app id " + appid)
63
64     nosigningkey = False
65     if not common.options.nosign:
66         if 'repo_keyalias' not in common.config:
67             nosigningkey = True
68             logging.critical("'repo_keyalias' not found in config.py!")
69         if 'keystore' not in common.config:
70             nosigningkey = True
71             logging.critical("'keystore' not found in config.py!")
72         if 'keystorepass' not in common.config:
73             nosigningkey = True
74             logging.critical("'keystorepass' not found in config.py!")
75         if 'keypass' not in common.config:
76             nosigningkey = True
77             logging.critical("'keypass' not found in config.py!")
78         if not os.path.exists(common.config['keystore']):
79             nosigningkey = True
80             logging.critical("'" + common.config['keystore'] + "' does not exist!")
81         if nosigningkey:
82             logging.warning("`fdroid update` requires a signing key, you can create one using:")
83             logging.warning("\tfdroid update --create-key")
84             sys.exit(1)
85
86     repodict = collections.OrderedDict()
87     repodict['timestamp'] = datetime.utcnow()
88     repodict['version'] = METADATA_VERSION
89
90     if common.config['repo_maxage'] != 0:
91         repodict['maxage'] = common.config['repo_maxage']
92
93     if archive:
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)
99     else:
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)
105
106     mirrorcheckfailed = False
107     mirrors = []
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 '" + mirror + "' does not end with 'fdroid'!")
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))
116         else:
117             mirrors.append(urllib.parse.urljoin(mirror + '/', urlbasepath))
118     for mirror in common.config.get('servergitmirrors', []):
119         mirror = get_raw_mirror(mirror)
120         if mirror is not None:
121             mirrors.append(mirror + '/')
122     if mirrorcheckfailed:
123         sys.exit(1)
124     if mirrors:
125         repodict['mirrors'] = mirrors
126
127     appsWithPackages = collections.OrderedDict()
128     for packageName in sortedids:
129         app = apps[packageName]
130         if app['Disabled']:
131             continue
132
133         # only include apps with packages
134         for apk in apks:
135             if apk['packageName'] == packageName:
136                 newapp = copy.copy(app)  # update wiki needs unmodified description
137                 newapp['Description'] = metadata.description_html(app['Description'],
138                                                                   _resolve_description_link)
139                 appsWithPackages[packageName] = newapp
140                 break
141
142     requestsdict = collections.OrderedDict()
143     for command in ('install', 'uninstall'):
144         packageNames = []
145         key = command + '_list'
146         if key in common.config:
147             if isinstance(common.config[key], str):
148                 packageNames = [common.config[key]]
149             elif all(isinstance(item, str) for item in common.config[key]):
150                 packageNames = common.config[key]
151             else:
152                 raise TypeError('only accepts strings, lists, and tuples')
153         requestsdict[command] = packageNames
154
155     make_v0(appsWithPackages, apks, repodir, repodict, requestsdict)
156     make_v1(appsWithPackages, apks, repodir, repodict, requestsdict)
157
158
159 def make_v1(apps, packages, repodir, repodict, requestsdict):
160
161     def _index_encoder_default(obj):
162         if isinstance(obj, set):
163             return list(obj)
164         if isinstance(obj, datetime):
165             return int(obj.timestamp() * 1000)  # Java expects milliseconds
166         raise TypeError(repr(obj) + " is not JSON serializable")
167
168     output = collections.OrderedDict()
169     output['repo'] = repodict
170     output['requests'] = requestsdict
171
172     appslist = []
173     output['apps'] = appslist
174     for appid, appdict in apps.items():
175         d = collections.OrderedDict()
176         appslist.append(d)
177         for k, v in sorted(appdict.items()):
178             if not v:
179                 continue
180             if k in ('builds', 'comments', 'metadatapath',
181                      'ArchivePolicy', 'AutoUpdateMode', 'MaintainerNotes',
182                      'Provides', 'Repo', 'RepoType', 'RequiresRoot',
183                      'UpdateCheckData', 'UpdateCheckIgnore', 'UpdateCheckMode',
184                      'UpdateCheckName', 'NoSourceSince', 'VercodeOperation'):
185                 continue
186
187             # name things after the App class fields in fdroidclient
188             if k == 'id':
189                 k = 'packageName'
190             elif k == 'CurrentVersionCode':  # TODO make SuggestedVersionCode the canonical name
191                 k = 'suggestedVersionCode'
192             elif k == 'CurrentVersion':  # TODO make SuggestedVersionName the canonical name
193                 k = 'suggestedVersionName'
194             elif k == 'AutoName':
195                 if 'Name' not in apps[appid]:
196                     d['name'] = v
197                 continue
198             else:
199                 k = k[:1].lower() + k[1:]
200             d[k] = v
201
202     output_packages = collections.OrderedDict()
203     output['packages'] = output_packages
204     for package in packages:
205         packageName = package['packageName']
206         if packageName in output_packages:
207             packagelist = output_packages[packageName]
208         else:
209             packagelist = []
210             output_packages[packageName] = packagelist
211         d = collections.OrderedDict()
212         packagelist.append(d)
213         for k, v in sorted(package.items()):
214             if not v:
215                 continue
216             if k in ('icon', 'icons', 'icons_src', 'name', ):
217                 continue
218             d[k] = v
219
220     json_name = 'index-v1.json'
221     index_file = os.path.join(repodir, json_name)
222     with open(index_file, 'w') as fp:
223         if common.options.pretty:
224             json.dump(output, fp, default=_index_encoder_default, indent=2)
225         else:
226             json.dump(output, fp, default=_index_encoder_default)
227
228     if common.options.nosign:
229         logging.debug('index-v1 must have a signature, use `fdroid signindex` to create it!')
230     else:
231         signindex.config = common.config
232         signindex.sign_index_v1(repodir, json_name)
233
234
235 def make_v0(apps, apks, repodir, repodict, requestsdict):
236     """
237     aka index.jar aka index.xml
238     """
239
240     doc = Document()
241
242     def addElement(name, value, doc, parent):
243         el = doc.createElement(name)
244         el.appendChild(doc.createTextNode(value))
245         parent.appendChild(el)
246
247     def addElementNonEmpty(name, value, doc, parent):
248         if not value:
249             return
250         addElement(name, value, doc, parent)
251
252     def addElementIfInApk(name, apk, key, doc, parent):
253         if key not in apk:
254             return
255         value = str(apk[key])
256         addElement(name, value, doc, parent)
257
258     def addElementCDATA(name, value, doc, parent):
259         el = doc.createElement(name)
260         el.appendChild(doc.createCDATASection(value))
261         parent.appendChild(el)
262
263     root = doc.createElement("fdroid")
264     doc.appendChild(root)
265
266     repoel = doc.createElement("repo")
267
268     repoel.setAttribute("name", repodict['name'])
269     if 'maxage' in repodict:
270         repoel.setAttribute("maxage", str(repodict['maxage']))
271     repoel.setAttribute("icon", os.path.basename(repodict['icon']))
272     repoel.setAttribute("url", repodict['address'])
273     addElement('description', repodict['description'], doc, repoel)
274     for mirror in repodict.get('mirrors', []):
275         addElement('mirror', mirror, doc, repoel)
276
277     repoel.setAttribute("version", str(repodict['version']))
278     repoel.setAttribute("timestamp", '%d' % repodict['timestamp'].timestamp())
279
280     pubkey, repo_pubkey_fingerprint = extract_pubkey()
281     repoel.setAttribute("pubkey", pubkey.decode('utf-8'))
282     root.appendChild(repoel)
283
284     for command in ('install', 'uninstall'):
285         for packageName in requestsdict[command]:
286             element = doc.createElement(command)
287             root.appendChild(element)
288             element.setAttribute('packageName', packageName)
289
290     for appid, appdict in apps.items():
291         app = metadata.App(appdict)
292
293         if app.Disabled is not None:
294             continue
295
296         # Get a list of the apks for this app...
297         apklist = []
298         for apk in apks:
299             if apk['packageName'] == appid:
300                 apklist.append(apk)
301
302         if len(apklist) == 0:
303             continue
304
305         apel = doc.createElement("application")
306         apel.setAttribute("id", app.id)
307         root.appendChild(apel)
308
309         addElement('id', app.id, doc, apel)
310         if app.added:
311             addElement('added', app.added.strftime('%Y-%m-%d'), doc, apel)
312         if app.lastUpdated:
313             addElement('lastupdated', app.lastUpdated.strftime('%Y-%m-%d'), doc, apel)
314         addElement('name', app.Name, doc, apel)
315         addElement('summary', app.Summary, doc, apel)
316         if app.icon:
317             addElement('icon', app.icon, doc, apel)
318
319         if app.get('Description'):
320             description = app.Description
321         else:
322             description = '<p>No description available</p>'
323         addElement('desc', description, doc, apel)
324         addElement('license', app.License, doc, apel)
325         if app.Categories:
326             addElement('categories', ','.join(app.Categories), doc, apel)
327             # We put the first (primary) category in LAST, which will have
328             # the desired effect of making clients that only understand one
329             # category see that one.
330             addElement('category', app.Categories[0], doc, apel)
331         addElement('web', app.WebSite, doc, apel)
332         addElement('source', app.SourceCode, doc, apel)
333         addElement('tracker', app.IssueTracker, doc, apel)
334         addElementNonEmpty('changelog', app.Changelog, doc, apel)
335         addElementNonEmpty('author', app.AuthorName, doc, apel)
336         addElementNonEmpty('email', app.AuthorEmail, doc, apel)
337         addElementNonEmpty('donate', app.Donate, doc, apel)
338         addElementNonEmpty('bitcoin', app.Bitcoin, doc, apel)
339         addElementNonEmpty('litecoin', app.Litecoin, doc, apel)
340         addElementNonEmpty('flattr', app.FlattrID, doc, apel)
341
342         # These elements actually refer to the current version (i.e. which
343         # one is recommended. They are historically mis-named, and need
344         # changing, but stay like this for now to support existing clients.
345         addElement('marketversion', app.CurrentVersion, doc, apel)
346         addElement('marketvercode', app.CurrentVersionCode, doc, apel)
347
348         if app.Provides:
349             pv = app.Provides.split(',')
350             addElementNonEmpty('provides', ','.join(pv), doc, apel)
351         if app.RequiresRoot:
352             addElement('requirements', 'root', doc, apel)
353
354         # Sort the apk list into version order, just so the web site
355         # doesn't have to do any work by default...
356         apklist = sorted(apklist, key=lambda apk: apk['versionCode'], reverse=True)
357
358         if 'antiFeatures' in apklist[0]:
359             app.AntiFeatures.extend(apklist[0]['antiFeatures'])
360         if app.AntiFeatures:
361             addElementNonEmpty('antifeatures', ','.join(app.AntiFeatures), doc, apel)
362
363         # Check for duplicates - they will make the client unhappy...
364         for i in range(len(apklist) - 1):
365             if apklist[i]['versionCode'] == apklist[i + 1]['versionCode']:
366                 logging.critical("duplicate versions: '%s' - '%s'" % (
367                     apklist[i]['apkName'], apklist[i + 1]['apkName']))
368                 sys.exit(1)
369
370         current_version_code = 0
371         current_version_file = None
372         for apk in apklist:
373             file_extension = common.get_file_extension(apk['apkName'])
374             # find the APK for the "Current Version"
375             if current_version_code < apk['versionCode']:
376                 current_version_code = apk['versionCode']
377             if current_version_code < int(app.CurrentVersionCode):
378                 current_version_file = apk['apkName']
379
380             apkel = doc.createElement("package")
381             apel.appendChild(apkel)
382             addElement('version', apk['versionName'], doc, apkel)
383             addElement('versioncode', str(apk['versionCode']), doc, apkel)
384             addElement('apkname', apk['apkName'], doc, apkel)
385             addElementIfInApk('srcname', apk, 'srcname', doc, apkel)
386
387             hashel = doc.createElement("hash")
388             hashel.setAttribute('type', 'sha256')
389             hashel.appendChild(doc.createTextNode(apk['hash']))
390             apkel.appendChild(hashel)
391
392             addElement('size', str(apk['size']), doc, apkel)
393             addElementIfInApk('sdkver', apk,
394                               'minSdkVersion', doc, apkel)
395             addElementIfInApk('targetSdkVersion', apk,
396                               'targetSdkVersion', doc, apkel)
397             addElementIfInApk('maxsdkver', apk,
398                               'maxSdkVersion', doc, apkel)
399             addElementIfInApk('obbMainFile', apk,
400                               'obbMainFile', doc, apkel)
401             addElementIfInApk('obbMainFileSha256', apk,
402                               'obbMainFileSha256', doc, apkel)
403             addElementIfInApk('obbPatchFile', apk,
404                               'obbPatchFile', doc, apkel)
405             addElementIfInApk('obbPatchFileSha256', apk,
406                               'obbPatchFileSha256', doc, apkel)
407             if 'added' in apk:
408                 addElement('added', apk['added'].strftime('%Y-%m-%d'), doc, apkel)
409
410             if file_extension == 'apk':  # sig is required for APKs, but only APKs
411                 addElement('sig', apk['sig'], doc, apkel)
412
413                 old_permissions = set()
414                 sorted_permissions = sorted(apk['uses-permission'])
415                 for perm in sorted_permissions:
416                     perm_name = perm.name
417                     if perm_name.startswith("android.permission."):
418                         perm_name = perm_name[19:]
419                     old_permissions.add(perm_name)
420                 addElementNonEmpty('permissions', ','.join(sorted(old_permissions)), doc, apkel)
421
422                 for permission in sorted_permissions:
423                     permel = doc.createElement('uses-permission')
424                     permel.setAttribute('name', permission.name)
425                     if permission.maxSdkVersion is not None:
426                         permel.setAttribute('maxSdkVersion', '%d' % permission.maxSdkVersion)
427                         apkel.appendChild(permel)
428                 for permission_sdk_23 in sorted(apk['uses-permission-sdk-23']):
429                     permel = doc.createElement('uses-permission-sdk-23')
430                     permel.setAttribute('name', permission_sdk_23.name)
431                     if permission_sdk_23.maxSdkVersion is not None:
432                         permel.setAttribute('maxSdkVersion', '%d' % permission_sdk_23.maxSdkVersion)
433                         apkel.appendChild(permel)
434                 if 'nativecode' in apk:
435                     addElement('nativecode', ','.join(sorted(apk['nativecode'])), doc, apkel)
436                 addElementNonEmpty('features', ','.join(sorted(apk['features'])), doc, apkel)
437
438         if current_version_file is not None \
439                 and common.config['make_current_version_link'] \
440                 and repodir == 'repo':  # only create these
441             namefield = common.config['current_version_name_source']
442             sanitized_name = re.sub(b'''[ '"&%?+=/]''', b'', app.get(namefield).encode('utf-8'))
443             apklinkname = sanitized_name + b'.apk'
444             current_version_path = os.path.join(repodir, current_version_file).encode('utf-8', 'surrogateescape')
445             if os.path.islink(apklinkname):
446                 os.remove(apklinkname)
447             os.symlink(current_version_path, apklinkname)
448             # also symlink gpg signature, if it exists
449             for extension in (b'.asc', b'.sig'):
450                 sigfile_path = current_version_path + extension
451                 if os.path.exists(sigfile_path):
452                     siglinkname = apklinkname + extension
453                     if os.path.islink(siglinkname):
454                         os.remove(siglinkname)
455                     os.symlink(sigfile_path, siglinkname)
456
457     if common.options.pretty:
458         output = doc.toprettyxml(encoding='utf-8')
459     else:
460         output = doc.toxml(encoding='utf-8')
461
462     with open(os.path.join(repodir, 'index.xml'), 'wb') as f:
463         f.write(output)
464
465     if 'repo_keyalias' in common.config:
466
467         if common.options.nosign:
468             logging.info("Creating unsigned index in preparation for signing")
469         else:
470             logging.info("Creating signed index with this key (SHA256):")
471             logging.info("%s" % repo_pubkey_fingerprint)
472
473         # Create a jar of the index...
474         jar_output = 'index_unsigned.jar' if common.options.nosign else 'index.jar'
475         p = FDroidPopen(['jar', 'cf', jar_output, 'index.xml'], cwd=repodir)
476         if p.returncode != 0:
477             logging.critical("Failed to create {0}".format(jar_output))
478             sys.exit(1)
479
480         # Sign the index...
481         signed = os.path.join(repodir, 'index.jar')
482         if common.options.nosign:
483             # Remove old signed index if not signing
484             if os.path.exists(signed):
485                 os.remove(signed)
486         else:
487             signindex.config = common.config
488             signindex.sign_jar(signed)
489
490     # Copy the repo icon into the repo directory...
491     icon_dir = os.path.join(repodir, 'icons')
492     iconfilename = os.path.join(icon_dir, os.path.basename(common.config['repo_icon']))
493     shutil.copyfile(common.config['repo_icon'], iconfilename)
494
495
496 def extract_pubkey():
497     """
498     Extracts and returns the repository's public key from the keystore.
499     :return: public key in hex, repository fingerprint
500     """
501     if 'repo_pubkey' in common.config:
502         pubkey = unhexlify(common.config['repo_pubkey'])
503     else:
504         env_vars = {'FDROID_KEY_STORE_PASS': common.config['keystorepass']}
505         p = FDroidPopenBytes([common.config['keytool'], '-exportcert',
506                               '-alias', common.config['repo_keyalias'],
507                               '-keystore', common.config['keystore'],
508                               '-storepass:env', 'FDROID_KEY_STORE_PASS']
509                              + common.config['smartcardoptions'],
510                              envs=env_vars, output=False, stderr_to_stdout=False)
511         if p.returncode != 0 or len(p.output) < 20:
512             msg = "Failed to get repo pubkey!"
513             if common.config['keystore'] == 'NONE':
514                 msg += ' Is your crypto smartcard plugged in?'
515             logging.critical(msg)
516             sys.exit(1)
517         pubkey = p.output
518     repo_pubkey_fingerprint = common.get_cert_fingerprint(pubkey)
519     return hexlify(pubkey), repo_pubkey_fingerprint
520
521
522 # Get raw URL from git service for mirroring
523 def get_raw_mirror(url):
524     # Divide urls in parts
525     url = url.split("/")
526
527     # Get the hostname
528     hostname = url[2]
529
530     # fdroidserver will use always 'master' branch for git-mirroring
531     branch = "master"
532     folder = "fdroid"
533
534     if hostname == "github.com":
535         # Github like RAW url "https://raw.githubusercontent.com/user/repo/master/fdroid"
536         url[2] = "raw.githubusercontent.com"
537         url.extend([branch, folder])
538     elif hostname == "gitlab.com":
539         # Gitlab like RAW url "https://gitlab.com/user/repo/raw/master/fdroid"
540         url.extend(["raw", branch, folder])
541     else:
542         return None
543
544     url = "/".join(url)
545     return url
546
547
548 class VerificationException(Exception):
549     pass
550
551
552 def download_repo_index(url_str, verify_fingerprint=True):
553     """
554     Downloads the repository index from the given :param url_str
555     and verifies the repository's fingerprint if :param verify_fingerprint is not False.
556
557     :raises: VerificationException() if the repository could not be verified
558
559     :return: The index in JSON format.
560     """
561     url = urllib.parse.urlsplit(url_str)
562
563     fingerprint = None
564     if verify_fingerprint:
565         query = urllib.parse.parse_qs(url.query)
566         if 'fingerprint' not in query:
567             raise VerificationException("No fingerprint in URL.")
568         fingerprint = query['fingerprint'][0]
569
570     url = urllib.parse.SplitResult(url.scheme, url.netloc, url.path + '/index-v1.jar', '', '')
571     r = requests.get(url.geturl())
572
573     with tempfile.NamedTemporaryFile() as fp:
574         # write and open JAR file
575         fp.write(r.content)
576         jar = zipfile.ZipFile(fp)
577
578         # verify that the JAR signature is valid
579         verify_jar_signature(fp.name)
580
581         # get public key and its fingerprint from JAR
582         public_key, public_key_fingerprint = get_public_key_from_jar(jar)
583
584         # compare the fingerprint if verify_fingerprint is True
585         if verify_fingerprint and fingerprint.upper() != public_key_fingerprint:
586             raise VerificationException("The repository's fingerprint does not match.")
587
588         # load repository index from JSON
589         index = json.loads(jar.read('index-v1.json').decode("utf-8"))
590         index["repo"]["pubkey"] = hexlify(public_key).decode("utf-8")
591         index["repo"]["fingerprint"] = public_key_fingerprint
592
593         # turn the apps into App objects
594         index["apps"] = [metadata.App(app) for app in index["apps"]]
595
596         return index
597
598
599 def verify_jar_signature(file):
600     """
601     Verifies the signature of a given JAR file.
602
603     :raises: VerificationException() if the JAR's signature could not be verified
604     """
605     if not common.verify_apk_signature(file, jar=True):
606         raise VerificationException("The repository's index could not be verified.")
607
608
609 def get_public_key_from_jar(jar):
610     """
611     Get the public key and its fingerprint from a JAR file.
612
613     :raises: VerificationException() if the JAR was not signed exactly once
614
615     :param jar: a zipfile.ZipFile object
616     :return: the public key from the jar and its fingerprint
617     """
618     # extract certificate from jar
619     certs = [n for n in jar.namelist() if common.CERT_PATH_REGEX.match(n)]
620     if len(certs) < 1:
621         raise VerificationException("Found no signing certificates for repository.")
622     if len(certs) > 1:
623         raise VerificationException("Found multiple signing certificates for repository.")
624
625     # extract public key from certificate
626     public_key = common.get_certificate(jar.read(certs[0]))
627     public_key_fingerprint = common.get_cert_fingerprint(public_key).replace(' ', '')
628
629     return public_key, public_key_fingerprint