Commit 833d0a6b authored by Léo-Paul Géneau's avatar Léo-Paul Géneau 👾

cli: add signature verification

Use libnetworkcache to list only binaries with verified signature
parent a9956d0a
...@@ -31,7 +31,6 @@ import ast ...@@ -31,7 +31,6 @@ import ast
import hashlib import hashlib
import json import json
import re import re
import requests
import sys import sys
import prettytable import prettytable
...@@ -63,8 +62,12 @@ class CacheLookupCommand(ConfigCommand): ...@@ -63,8 +62,12 @@ class CacheLookupCommand(ConfigCommand):
def take_action(self, args): def take_action(self, args):
configp = self.fetch_config(args) configp = self.fetch_config(args)
cache_dir = configp.get('networkcache', 'download-binary-dir-url') cache_dir = configp.get('networkcache', 'download-binary-dir-url')
cache_url = configp.get('networkcache', 'download-binary-cache-url')
signature_certificate_list = configp.get('networkcache', 'signature-certificate-list')
sys.exit( sys.exit(
do_lookup(self.app.log, cache_dir, args.software_url)) do_lookup(self.app.log, cache_dir, cache_url,
signature_certificate_list, args.software_url,))
def looks_like_md5(s): def looks_like_md5(s):
""" """
...@@ -74,38 +77,29 @@ def looks_like_md5(s): ...@@ -74,38 +77,29 @@ def looks_like_md5(s):
return re.match('[0-9a-f]{32}', s) return re.match('[0-9a-f]{32}', s)
def ostuple(jsondict): def ostuple(info_dict):
srdict = json.loads(jsondict) return (info_dict['machine'],) + ast.literal_eval(info_dict['os'])
return (srdict['machine'],) + ast.literal_eval(srdict['os'])
def do_lookup(logger, cache_dir, software_url): def do_lookup(logger, cache_dir, cache_url, signature_certificate_list,
software_url):
if looks_like_md5(software_url): if looks_like_md5(software_url):
md5 = software_url md5 = software_url
else: else:
md5 = hashlib.md5(str2bytes(software_url)).hexdigest() md5 = hashlib.md5(str2bytes(software_url)).hexdigest()
try: try:
url = '%s/%s' % (cache_dir, md5) entries = list(
logger.debug('Connecting to %s', url) networkcache.download_entry_list(cache_url, cache_dir, md5, logger,
req = requests.get(url, timeout=5) signature_certificate_list, software_url))
except (requests.Timeout, requests.ConnectionError): except Exception:
logger.critical('Cannot connect to cache server at %s', url) logger.critical('Error while looking object %s', software_url,
return FAILURE_EXIT_CODE exc_info=True)
if not req.ok:
if req.status_code == 404:
logger.critical('Object not in cache: %s', software_url)
else:
logger.critical('Error while looking object %s: %s', software_url, req.reason)
return FAILURE_EXIT_CODE return FAILURE_EXIT_CODE
entries = req.json()
if not entries: if not entries:
logger.info('Object found in cache, but has no binary entries.') logger.info('Object found in cache, but has no binary entries.')
return 0 return 0
ostable = sorted(ostuple(entry[0]) for entry in entries) ostable = sorted(ostuple(json.loads(entry[0])) for entry in entries)
pt = prettytable.PrettyTable(['machine', 'distribution', 'version', 'id', 'compatible?']) pt = prettytable.PrettyTable(['machine', 'distribution', 'version', 'id', 'compatible?'])
for os in ostable: for os in ostable:
......
...@@ -57,6 +57,13 @@ def is_compatible(machine, os): ...@@ -57,6 +57,13 @@ def is_compatible(machine, os):
return machine == platform.machine() and os_matches(os, distribution_tuple()) return machine == platform.machine() and os_matches(os, distribution_tuple())
def download_entry_list(cache_url, dir_url, key, logger,
signature_certificate_list, software_url):
nc = NetworkcacheClient(cache_url, dir_url,
signature_certificate_list=signature_certificate_list or None)
return nc.select_generic(key)
@fallback_call @fallback_call
def download_network_cached(cache_url, dir_url, software_url, software_root, def download_network_cached(cache_url, dir_url, software_url, software_root,
key, path, logger, signature_certificate_list, key, path, logger, signature_certificate_list,
...@@ -172,7 +179,7 @@ def upload_network_cached(software_root, software_url, cached_key, ...@@ -172,7 +179,7 @@ def upload_network_cached(software_root, software_url, cached_key,
try: try:
return nc.upload_generic(f, cached_key, **kw) return nc.upload_generic(f, cached_key, **kw)
except (IOError, UploadError) as e: except (IOError, UploadError) as e:
logger.info('Failed to upload file. %s' % (str(e))) logger.info('Failed to upload file. %s' % str(e))
return False return False
finally: finally:
f.close() f.close()
......
...@@ -63,6 +63,21 @@ import slapos.slap ...@@ -63,6 +63,21 @@ import slapos.slap
import supervisor.supervisorctl import supervisor.supervisorctl
signature_certificate_list = """-----BEGIN CERTIFICATE-----
MIIB9jCCAV+gAwIBAgIJAKRvzcy7OH0UMA0GCSqGSIb3DQEBBQUAMBMxETAPBgNV
BAMMCENPTVAtNzcyMCAXDTEyMDgxMDE1NDI1MVoYDzIxMTIwNzE3MTU0MjUxWjAT
MREwDwYDVQQDDAhDT01QLTc3MjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
o7aipd6MbnuGDeR1UJUjuMLQUariAyQ2l2ZDS6TfOwjHiPw/mhzkielgk73kqN7A
sUREx41eTcYCXzTq3WP3xCLE4LxLg1eIhd4nwNHj8H18xR9aP0AGjo4UFl5BOMa1
mwoyBt3VtfGtUmb8whpeJgHhqrPPxLoON+i6fIbXDaUCAwEAAaNQME4wHQYDVR0O
BBYEFEfjy3OopT2lOksKmKBNHTJE2hFlMB8GA1UdIwQYMBaAFEfjy3OopT2lOksK
mKBNHTJE2hFlMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAaNRx6YN2
M/p3R8/xS6zvH1EqJ3FFD7XeAQ52WuQnKSREzuw0dsw12ClxjcHiQEFioyTiTtjs
5pW18Ry5Ie7iFK4cQMerZwWPxBodEbAteYlRsI6kePV7Gf735Y1RpuN8qZ2sYL6e
x2IMeSwJ82BpdEI5niXxB+iT0HxhmR+XaMI=
-----END CERTIFICATE-----
"""
def raiseNotFoundError(*args, **kwargs): def raiseNotFoundError(*args, **kwargs):
raise slapos.slap.NotFoundError() raise slapos.slap.NotFoundError()
...@@ -72,6 +87,7 @@ class CliMixin(unittest.TestCase): ...@@ -72,6 +87,7 @@ class CliMixin(unittest.TestCase):
self.logger = create_autospec(logging.Logger) self.logger = create_autospec(logging.Logger)
self.local = {'slap': slap, 'product': SoftwareProductCollection(self.logger, slap)} self.local = {'slap': slap, 'product': SoftwareProductCollection(self.logger, slap)}
self.conf = create_autospec(ClientConfig) self.conf = create_autospec(ClientConfig)
self.sign_cert_list = signature_certificate_list
class TestCliCache(CliMixin): class TestCliCache(CliMixin):
...@@ -80,7 +96,9 @@ class TestCliCache(CliMixin): ...@@ -80,7 +96,9 @@ class TestCliCache(CliMixin):
self.assertEqual(0, cache_do_lookup( self.assertEqual(0, cache_do_lookup(
self.logger, self.logger,
cache_dir="http://dir.shacache.org", cache_dir="http://dir.shacache.org",
software_url=self.test_url)) cache_url="http://shacache.org",
software_url=self.test_url,
signature_certificate_list=self.sign_cert_list))
self.logger.info.assert_any_call('Software URL: %s', self.logger.info.assert_any_call('Software URL: %s',
u'https://lab.nexedi.com/nexedi/slapos/raw/1.0.102/software/slaprunner/software.cfg') u'https://lab.nexedi.com/nexedi/slapos/raw/1.0.102/software/slaprunner/software.cfg')
...@@ -97,19 +115,25 @@ class TestCliCache(CliMixin): ...@@ -97,19 +115,25 @@ class TestCliCache(CliMixin):
self.assertEqual(10, cache_do_lookup( self.assertEqual(10, cache_do_lookup(
self.logger, self.logger,
cache_dir="http://dir.shacache.org", cache_dir="http://dir.shacache.org",
software_url="this_is_uncached_url")) cache_url="http://shacache.org",
software_url="this_is_uncached_url",
signature_certificate_list=self.sign_cert_list))
self.logger.critical.assert_any_call('Object not in cache: %s', 'this_is_uncached_url') self.logger.critical.assert_any_call(
'Error while looking object %s', 'this_is_uncached_url', exc_info=True)
def test_bad_cache_dir(self): def test_bad_cache_dir(self):
self.assertEqual(10, cache_do_lookup( self.assertEqual(10, cache_do_lookup(
self.logger, self.logger,
cache_dir="http://xxx.shacache.org", cache_dir="http://xxx.shacache.org",
software_url=self.test_url)) cache_url="http://shacache.org",
software_url=self.test_url,
signature_certificate_list=self.sign_cert_list))
self.logger.critical.assert_any_call( self.logger.critical.assert_any_call(
'Cannot connect to cache server at %s', 'Error while looking object %s',
'http://xxx.shacache.org/cccdc51a07e8c575c880f2d70dd4d458') 'https://lab.nexedi.com/nexedi/slapos/raw/1.0.102/software/slaprunner/software.cfg',
exc_info=True)
class TestCliCacheSource(CliMixin): class TestCliCacheSource(CliMixin):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment