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