From f4601e7b050f7e6ab2e2548fc26cb8bbd64947a9 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 8 Mar 2016 23:19:52 +0530 Subject: [PATCH] monkeysphere: Reorganize around keys instead - Read Apache configuration to find the list of all available certificates and their associated domains. Use this for setting UIDs properly. - Solve the issue of re-importing renewed certficiate. Use the SSH fingerprints as unique keys instead of domain names. Compute SSH fingerprints for SSH keys and HTTPS certficates inorder accurately identify if they are currently imported into monkeysphere. - Allow having more than one domains for a certficiate. Add action to import new domains to an existing monkeysphere OpenPGP key. - Import only once for a given certficiate and keep adding UIDs when domains get added. - Merge services SSH and HTTPS giving us the ability to deals with many more services. Remove special handling for different kinds of certificate sources. - Supress monkeysphere prompts in case of reusing UIDs. --- actions/monkeysphere | 195 +++++++++++------ .../monkeysphere/templates/monkeysphere.html | 203 +++++++----------- .../templates/monkeysphere_details.html | 38 +++- plinth/modules/monkeysphere/urls.py | 8 +- plinth/modules/monkeysphere/views.py | 130 ++++------- 5 files changed, 278 insertions(+), 296 deletions(-) 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):