diff --git a/actions/monkeysphere b/actions/monkeysphere index ef37de3f7..ed280dc85 100755 --- a/actions/monkeysphere +++ b/actions/monkeysphere @@ -21,8 +21,10 @@ Configuration helper for monkeysphere. """ import argparse +import augeas import json import os +import re import subprocess @@ -32,24 +34,16 @@ def parse_arguments(): subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') host_show_keys = subparsers.add_parser( - 'host-show-keys', help='Show host key fingerprints') + 'host-show-keys', help='Show imported/importable keys') host_show_keys.add_argument( - 'key_ids', nargs='*', help='Optional list of KEYIDs') + 'key_id', nargs='?', help='Optional KEYID to retrieve details for') - host_import_ssh_key = subparsers.add_parser( - 'host-import-ssh-key', help='Import host SSH key') - host_import_ssh_key.add_argument( - 'domain', help='Fully-qualified domain name') - - host_import_snakeoil_key = subparsers.add_parser( - 'host-import-snakeoil-key', help='Import host snakeoil key') - host_import_snakeoil_key.add_argument( - 'domain', help='Fully-qualified domain name') - - host_import_letsencrypt_key = subparsers.add_parser( - 'host-import-letsencrypt-key', help="Import Let's Encrypt key") - host_import_letsencrypt_key.add_argument( - 'domain', help='Fully-qualified domain name') + host_import_key = subparsers.add_parser( + 'host-import-key', help='Import a key into monkeysphere') + host_import_key.add_argument( + 'ssh_fingerprint', help='SSH fingerprint of the key to import') + host_import_key.add_argument( + 'domains', nargs='*', help='List of available domains') host_publish_key = subparsers.add_parser( 'host-publish-key', help='Push host key to keyserver') @@ -59,16 +53,75 @@ def parse_arguments(): return parser.parse_args() -def subcommand_host_show_keys(arguments): - """Show host key fingerprints.""" - try: +def get_ssh_keys(): + """Return all SSH keys.""" + keys = {} + + key_files = ['/etc/ssh/ssh_host_rsa_key'] + for key_file in key_files: output = subprocess.check_output( - ['monkeysphere-host', 'show-keys'] + arguments.key_ids, + ['ssh-keygen', '-l', '-E', 'MD5', '-f', key_file]) + fingerprint = output.decode().split()[1].lstrip('MD5:') + keys[fingerprint] = {'ssh_fingerprint': fingerprint, + 'service': 'ssh', + 'key_file': key_file, + 'available_domains': ['*']} + + return keys + + +def get_pem_ssh_fingerprint(pem_file): + """Return the SSH fingerprint of a PEM file.""" + public_key = subprocess.check_output( + ['openssl', 'rsa', '-in', pem_file, '-pubout'], + stderr=subprocess.DEVNULL) + ssh_public_key = subprocess.check_output( + ['ssh-keygen', '-i', '-m', 'PKCS8', '-f', '/dev/stdin'], + input=public_key) + fingerprint = subprocess.check_output( + ['ssh-keygen', '-l', '-E', 'md5', '-f', '/dev/stdin'], + input=ssh_public_key) + + return fingerprint.decode().split()[1].lstrip('MD5:') + + +def get_https_keys(): + """Return all HTTPS keys.""" + aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + + augeas.Augeas.NO_MODL_AUTOLOAD) + aug.set('/augeas/load/Httpd/lens', 'Httpd.lns') + aug.set('/augeas/load/Httpd/incl[last() + 1]', + '/etc/apache2/sites-available/*') + aug.load() + + keys = {} + path = '/files/etc/apache2/sites-available//VirtualHost' + for match in aug.match(path): + host = {'available_domains': ['*'], 'service': 'https'} + for directive in aug.match(match + '/directive'): + name = aug.get(directive) + if name == 'ServerName': + host['available_domains'] = [aug.get(directive + '/arg')] + elif name in ('GnuTLSKeyFile', 'SSLCertificateKeyFile'): + host['key_file'] = aug.get(directive + '/arg') + + if 'key_file' in host: + host['ssh_fingerprint'] = get_pem_ssh_fingerprint(host['key_file']) + keys[host['ssh_fingerprint']] = host + + return keys + + +def get_monkeysphere_keys(key_id=None): + """Return the list of keys imported into monkeysphere.""" + try: + key_ids = [] if not key_id else [key_id] + output = subprocess.check_output( + ['monkeysphere-host', 'show-keys'] + key_ids, stderr=subprocess.DEVNULL) except subprocess.CalledProcessError: # no keys available - print(json.dumps({'keys': []})) - return + return {} # parse output keys = [dict()] @@ -79,9 +132,15 @@ def subcommand_host_show_keys(arguments): keys[-1]['pub'] = data[0] keys[-1]['date'] = data[1] elif line.startswith('uid'): - keys[-1]['uid'] = line.lstrip('uid').strip() + uid = line.lstrip('uid').strip() + keys[-1].setdefault('uids', []).append(uid) + matches = re.match('([a-zA-Z]*)://(.*)(:\d*)?', uid) + keys[-1]['service'] = matches.group(1) + keys[-1].setdefault('imported_domains', []) \ + .append(matches.group(2)) elif line.startswith('OpenPGP fingerprint:'): - keys[-1]['pgp_fingerprint'] = line.lstrip('Open PGP fingerprint:') + keys[-1]['openpgp_fingerprint'] = \ + line.lstrip('OpenPGP fingerprint:') elif line.startswith('ssh fingerprint:'): data = line.lstrip('ssh fingerprint:').split() keys[-1]['ssh_key_size'] = data[0] @@ -90,52 +149,60 @@ def subcommand_host_show_keys(arguments): elif line == '': keys.append(dict()) - print(json.dumps({'keys': keys})) + return {key['ssh_fingerprint']: key for key in keys} -def subcommand_host_import_ssh_key(arguments): +def get_merged_keys(key_id=None): + """Return merged list of system and monkeysphere keys.""" + keys = get_monkeysphere_keys(key_id) + + system_keys = list(get_ssh_keys().items()) + list(get_https_keys().items()) + for ssh_fingerprint, key in system_keys: + if key_id and ssh_fingerprint not in keys: + continue + + if ssh_fingerprint in keys: + keys[ssh_fingerprint].update({ + 'available_domains': key['available_domains'], + 'key_file': key['key_file']}) + else: + keys[ssh_fingerprint] = key + + return keys + + +def subcommand_host_show_keys(arguments): + """Show host key fingerprints.""" + print(json.dumps({'keys': get_merged_keys(arguments.key_id)})) + + +def subcommand_host_import_key(arguments, second_run=False): """Import host SSH key.""" - output = subprocess.check_output( - ['monkeysphere-host', 'import-key', - '/etc/ssh/ssh_host_rsa_key', 'ssh://' + arguments.domain]) - print(output.decode()) + keys = get_merged_keys() + if arguments.ssh_fingerprint not in keys: + raise Exception('Unknown SSH fingerprint') + key = keys[arguments.ssh_fingerprint] + if '*' in key['available_domains']: + key['available_domains'] = arguments.domains -def subcommand_host_import_snakeoil_key(arguments): - """Import host snakeoil key.""" - proc = subprocess.Popen( - ['monkeysphere-host', 'import-key', - '/etc/ssl/private/ssl-cert-snakeoil.key', - 'https://' + arguments.domain], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - env=dict( - os.environ, - MONKEYSPHERE_PROMPT='false')) - output, error = proc.communicate() - output, error = output.decode(), error.decode() - if proc.returncode != 0: - raise Exception(output, error) + if 'openpgp_fingerprint' not in key and not second_run: + env = dict(os.environ, MONKEYSPHERE_PROMPT='false') + subprocess.check_call( + ['monkeysphere-host', 'import-key', + key['key_file'], key['service'] + '://' + + key['available_domains'][0]], env=env) + subcommand_host_import_key(arguments, second_run=True) + else: + for domain in key['available_domains']: + if domain in key['imported_domains']: + continue - print(output) - - -def subcommand_host_import_letsencrypt_key(arguments): - """Import Let's Encrypt key.""" - proc = subprocess.Popen( - ['monkeysphere-host', 'import-key', - os.path.join('/etc/letsencrypt/live', - arguments.domain, 'privkey.pem'), - 'https://' + arguments.domain], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - env=dict( - os.environ, - MONKEYSPHERE_PROMPT='false')) - output, error = proc.communicate() - output, error = output.decode(), error.decode() - if proc.returncode != 0: - raise Exception(output, error) - - print(output) + env = dict(os.environ, MONKEYSPHERE_PROMPT='false') + subprocess.check_call( + ['monkeysphere-host', 'add-servicename', + key['service'] + '://' + domain, key['openpgp_fingerprint']], + env=env) def subcommand_host_publish_key(arguments): diff --git a/plinth/modules/monkeysphere/templates/monkeysphere.html b/plinth/modules/monkeysphere/templates/monkeysphere.html index d9a0204f2..ec9985601 100644 --- a/plinth/modules/monkeysphere/templates/monkeysphere.html +++ b/plinth/modules/monkeysphere/templates/monkeysphere.html @@ -27,6 +27,25 @@ {% endif %} + + {% endblock %} @@ -47,155 +66,87 @@

{% endif %} -

{% trans "Secure Shell (SSH)" %}

-
-
- +
+
- + + - + - {% for domain in status.domains %} + {% for key in status.keys.values %} - + + - - {% endfor %} - -
{% trans "Domain" %}{% trans "Service" %}{% trans "Domains" %} {% trans "OpenPGP Fingerprint" %}{% trans "Actions" %}
{{ domain.name }} - {% if domain.key %} - - {{ domain.key.pgp_fingerprint }} - + {% if key.service == 'ssh' %} + {% trans "Secure Shell" %} + {% elif key.service == 'https' %} + {% trans "Web Server" %} {% else %} - {% trans "Not Available" %} + {% trans "Other" %} {% endif %} - {% if not domain.key %} -
+
    + {% for domain in key.available_domains %} +
  • + {% if domain in key.imported_domains %} + + {% else %} + + {% endif %} + {{ domain }} +
  • + {% endfor %} +
+
+ {% if key.openpgp_fingerprint %} + + {{ key.openpgp_fingerprint }} + + {% else %} + {% trans "-" %} + {% endif %} + + {% if not key.openpgp_fingerprint %} + {% csrf_token %} + {% trans "Import Key" %} - {% elif not running %} -
+ {% else %} + {% if not running %} + {% csrf_token %}
- {% endif %} -
-
-
+ {% endif %} + {% if key.imported_domains != key.available_domains %} +
+ {% csrf_token %} -

{% trans "Secure Web Server (HTTPS)" %}

- -

{% trans "Self-signed Certificate" %}

- -
-
- - - - - - - - - - {% for domain in status.snakeoil_domains %} - - - - - - {% endfor %} - -
{% trans "Domain" %}{% trans "OpenPGP Fingerprint" %}{% trans "Actions" %}
{{ domain.name }} - {% if domain.key %} - - {{ domain.key.pgp_fingerprint }} - - {% else %} - {% trans "Not Available" %} - {% endif %} - - {% if not domain.key %} - - {% csrf_token %} - - - - {% elif not running %} -
- {% csrf_token %} - - -
- {% endif %} -
-
-
- -

{% trans "Let's Encrypt Certificates" %}

- -
-
- - - - - - - - - - {% for domain in status.letsencrypt_domains %} - - - - diff --git a/plinth/modules/monkeysphere/templates/monkeysphere_details.html b/plinth/modules/monkeysphere/templates/monkeysphere_details.html index bbe43947a..4702383c5 100644 --- a/plinth/modules/monkeysphere/templates/monkeysphere_details.html +++ b/plinth/modules/monkeysphere/templates/monkeysphere_details.html @@ -29,18 +29,18 @@ - + - - + + - + @@ -55,6 +55,30 @@ + + + + + + + + + + + + + + + +
{% trans "Domain" %}{% trans "OpenPGP Fingerprint" %}{% trans "Actions" %}
{{ domain.name }} - {% if domain.key %} - - {{ domain.key.pgp_fingerprint }} - - {% else %} - {% trans "Not Available" %} - {% endif %} - - {% if not domain.key %} -
- {% csrf_token %} - - -
- {% elif not running %} -
- {% csrf_token %} - - -
+ + + {% endif %} {% endif %}
{% trans "OpenPGP Fingerprint" %}{{ key.pgp_fingerprint }}{{ key.openpgp_fingerprint }}
{% trans "OpenPGP Key ID" %} {{ key.pub }}
{% trans "OpenPGP User ID" %}{{ key.uid }}{% trans "OpenPGP User IDs" %}{{ key.uids|join:', ' }}
{% trans "Key Generation Date" %}{% trans "Key Import Date" %} {{ key.date }}
{% trans "SSH Fingerprint" %} {{ key.ssh_fingerprint }}
{% trans "Service" %} + {% if key.service == 'ssh' %} + {% trans "Secure Shell" %} + {% elif key.service == 'https' %} + {% trans "Web Server" %} + {% else %} + {% trans "Other" %} + {% endif %} +
{% trans "Key File" %}{{ key.key_file }}
{% trans "Available Domains" %}{{ key.available_domains|join:', ' }}
{% trans "Added Domains" %}{{ key.imported_domains|join:', ' }}
@@ -66,13 +90,13 @@

 # {% trans "Download the key" %}
-gpg --recv-key {{ key.pgp_fingerprint }}
+gpg --recv-key {{ key.openpgp_fingerprint }}
 
 # {% trans "Sign the key" %}
-gpg --sign-key {{ key.pgp_fingerprint }}
+gpg --sign-key {{ key.openpgp_fingerprint }}
 
 # {% trans "Send the key back to the keyservers" %}
-gpg --send-key {{ key.pgp_fingerprint }}
+gpg --send-key {{ key.openpgp_fingerprint }}
 
{% endblock %} diff --git a/plinth/modules/monkeysphere/urls.py b/plinth/modules/monkeysphere/urls.py index 73438b077..e02bfbb17 100644 --- a/plinth/modules/monkeysphere/urls.py +++ b/plinth/modules/monkeysphere/urls.py @@ -26,12 +26,8 @@ from . import views urlpatterns = [ url(r'^sys/monkeysphere/$', views.index, name='index'), - url(r'^sys/monkeysphere/(?P[^/]+)/generate/$', - views.generate, name='generate'), - url(r'^sys/monkeysphere/(?P[^/]+)/generate_snakeoil/$', - views.generate_snakeoil, name='generate_snakeoil'), - url(r'^sys/monkeysphere/(?P[^/]+)/generate_letsencrypt/$', - views.generate_letsencrypt, name='generate_letsencrypt'), + url(r'^sys/monkeysphere/(?P[0-9A-Fa-f:]+)/import/$', + views.import_key, name='import'), url(r'^sys/monkeysphere/(?P[0-9A-Fa-f]+)/details/$', views.details, name='details'), url(r'^sys/monkeysphere/(?P[0-9A-Fa-f]+)/publish/$', diff --git a/plinth/modules/monkeysphere/views.py b/plinth/modules/monkeysphere/views.py index a2a43f18f..d74899919 100644 --- a/plinth/modules/monkeysphere/views.py +++ b/plinth/modules/monkeysphere/views.py @@ -47,58 +47,27 @@ def index(request): @require_POST -def generate(request, domain): - """Generate OpenPGP key for SSH service.""" - valid_domain = any((domain in domains - for domains in names.domains.values())) - if valid_domain: - try: - actions.superuser_run( - 'monkeysphere', ['host-import-ssh-key', domain]) - messages.success(request, _('Generated OpenPGP key.')) - except actions.ActionError as exception: - messages.error(request, str(exception)) - - return redirect(reverse_lazy('monkeysphere:index')) - - -@require_POST -def generate_snakeoil(request, domain): - """Generate OpenPGP key for snakeoil certificate.""" - valid_domain = any((domain in domains - for domains in names.domains.values())) - if valid_domain: - try: - actions.superuser_run( - 'monkeysphere', ['host-import-snakeoil-key', domain]) - messages.success(request, _('Generated OpenPGP key.')) - except actions.ActionError as exception: - messages.error(request, str(exception)) - - return redirect(reverse_lazy('monkeysphere:index')) - - -@require_POST -def generate_letsencrypt(request, domain): - """Generate OpenPGP key for Let's Encrypt certificate.""" - valid_domain = any((domain in domains - for domains in names.domains.values())) - if valid_domain: - try: - actions.superuser_run( - 'monkeysphere', ['host-import-letsencrypt-key', domain]) - messages.success(request, _('Generated OpenPGP key.')) - except actions.ActionError as exception: - messages.error(request, str(exception)) +def import_key(request, ssh_fingerprint): + """Import a key into monkeysphere.""" + available_domains = [domain + for domains in names.domains.values() + for domain in domains] + try: + actions.superuser_run( + 'monkeysphere', ['host-import-key', ssh_fingerprint] + + available_domains) + messages.success(request, _('Imported key.')) + except actions.ActionError as exception: + messages.error(request, str(exception)) return redirect(reverse_lazy('monkeysphere:index')) def details(request, fingerprint): """Get details for an OpenPGP key.""" - key = get_key(fingerprint) return TemplateResponse(request, 'monkeysphere_details.html', - {'title': _('Monkeysphere'), 'key': key}) + {'title': monkeysphere.title, + 'key': get_key(fingerprint)}) @require_POST @@ -126,61 +95,36 @@ def cancel(request): def get_status(): """Get the current status.""" - output = actions.superuser_run('monkeysphere', ['host-show-keys']) - keys = {} - https_keys = {} - for key in json.loads(output)['keys']: - if key['uid'].startswith('ssh'): - key['name'] = key['uid'].replace('ssh://', '') - keys[key['name']] = key - elif key['uid'].startswith('https'): - key['name'] = key['uid'].replace('https://', '') - https_keys[key['name']] = key + return {'keys': get_keys()} - domains = [] - for domains_of_a_type in names.domains.values(): - for domain in domains_of_a_type: - domains.append({ - 'name': domain, - 'key': keys.get(domain), - }) - # XXX: Currently, there's no way to tell if keys in monkeysphere are for - # snakeoil or letsencrypt certs. If snakeoil cert is imported for a domain, - # then later that domain is activated for letsencrypt, the snakeoil cert - # will be shown in the letsencrypt table. - output = actions.superuser_run('letsencrypt', ['get-status']) - letsencrypt_domains_all = json.loads(output)['domains'] - letsencrypt_domains = [] - snakeoil_domains = [] - for domains_of_a_type in names.domains.values(): - for domain in domains_of_a_type: - if domain in letsencrypt_domains_all and \ - letsencrypt_domains_all[domain]['certificate_available']: - letsencrypt_domains.append({ - 'name': domain, - 'key': https_keys.get(domain), - }) - else: - snakeoil_domains.append({ - 'name': domain, - 'key': https_keys.get(domain), - }) +def get_keys(fingerprint=None): + """Get keys.""" + fingerprint = [fingerprint] if fingerprint else [] + output = actions.superuser_run('monkeysphere', + ['host-show-keys'] + fingerprint) + keys = json.loads(output)['keys'] - return {'domains': domains, - 'snakeoil_domains': snakeoil_domains, - 'letsencrypt_domains': letsencrypt_domains} + domains = [domain + for domains_of_a_type in names.domains.values() + for domain in domains_of_a_type] + for key in keys.values(): + if '*' in key['available_domains']: + key['available_domains'] = set(domains) + else: + key['available_domains'] = set(key['available_domains']) + + if 'imported_domains' in key: + key['imported_domains'] = set(key['imported_domains']) \ + .intersection(key['available_domains']) + + return keys def get_key(fingerprint): """Get key by fingerprint.""" - output = actions.superuser_run('monkeysphere', - ['host-show-keys', fingerprint]) - if output: - keys = json.loads(output)['keys'] - return keys[0] - - return None + keys = get_keys(fingerprint) + return list(keys.values())[0] if len(keys) else None def _collect_publish_result(request):