From 00396c158aa032585db88cfd4b622281ba3cbd7f Mon Sep 17 00:00:00 2001 From: mathieui Date: Thu, 11 Dec 2014 22:28:44 +0100 Subject: Fix #2847 (SASL External support) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add two new options, keyfile and certfile, which must be both set for the auth to work. - if both are set, then poezio doesn’t force-prompt a password if there is none specified - add /cert_add, /cert_fetch, /cert_disable, /cert_revoke and /certs commands. - add a page of documentation on the process --- src/config.py | 2 + src/connection.py | 16 ++++- src/core/handlers.py | 1 + src/tabs/rostertab.py | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/config.py b/src/config.py index 390fe7f1..86aae8ef 100644 --- a/src/config.py +++ b/src/config.py @@ -34,6 +34,7 @@ DEFAULT_CONFIG = { 'beep_on': 'highlight private invite', 'ca_cert_path': '', 'certificate': '', + 'certfile': '', 'ciphers': 'HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK:!SRP:!3DES:!aNULL', 'connection_check_interval': 60, 'connection_timeout_delay': 10, @@ -68,6 +69,7 @@ DEFAULT_CONFIG = { 'ignore_private': False, 'information_buffer_popup_on': 'error roster warning help info', 'jid': '', + 'keyfile': '', 'lang': 'en', 'lazy_resize': True, 'load_log': 10, diff --git a/src/connection.py b/src/connection.py index 1bbe632d..cd2ccedd 100644 --- a/src/connection.py +++ b/src/connection.py @@ -30,6 +30,10 @@ class Connection(slixmpp.ClientXMPP): __init = False def __init__(self): resource = config.get('resource') + + keyfile = config.get('keyfile') + certfile = config.get('certfile') + if config.get('jid'): # Field used to know if we are anonymous or not. # many features will be handled differently @@ -38,7 +42,9 @@ class Connection(slixmpp.ClientXMPP): jid = '%s' % config.get('jid') if resource: jid = '%s/%s'% (jid, resource) - password = config.get('password') or getpass.getpass() + password = config.get('password') + if not password and not (keyfile and certfile): + password = getpass.getpass() else: # anonymous auth self.anon = True jid = config.get('server') @@ -57,6 +63,13 @@ class Connection(slixmpp.ClientXMPP): self['feature_mechanisms'].unencrypted_cram = False self['feature_mechanisms'].unencrypted_scram = False + self.keyfile = config.get('keyfile') + self.certfile = config.get('certfile') + if keyfile and not certfile: + log.error('keyfile is present in configuration file without certfile') + elif certfile and not keyfile: + log.error('certfile is present in configuration file without keyfile') + self.core = None self.auto_reconnect = config.get('auto_reconnect') self.reconnect_max_attempts = 0 @@ -127,6 +140,7 @@ class Connection(slixmpp.ClientXMPP): self.register_plugin('xep_0202') self.register_plugin('xep_0224') self.register_plugin('xep_0249') + self.register_plugin('xep_0257') self.register_plugin('xep_0280') self.register_plugin('xep_0297') self.register_plugin('xep_0308') diff --git a/src/core/handlers.py b/src/core/handlers.py index 6b613a93..a1e8596c 100644 --- a/src/core/handlers.py +++ b/src/core/handlers.py @@ -58,6 +58,7 @@ def on_session_start_features(self, _): features = iq['disco_info']['features'] rostertab = self.get_tab_by_name('Roster', tabs.RosterInfoTab) rostertab.check_blocking(features) + rostertab.check_saslexternal(features) if (config.get('enable_carbons') and 'urn:xmpp:carbons:2' in features): self.xmpp.plugin['xep_0280'].enable() diff --git a/src/tabs/rostertab.py b/src/tabs/rostertab.py index 52a3a88c..846ba327 100644 --- a/src/tabs/rostertab.py +++ b/src/tabs/rostertab.py @@ -10,9 +10,11 @@ from gettext import gettext as _ import logging log = logging.getLogger(__name__) +import base64 import curses import difflib import os +import ssl from os import getenv, path from . import Tab @@ -146,6 +148,170 @@ class RosterInfoTab(Tab): self.core.xmpp.del_event_handler('session_start', self.check_blocking) self.core.xmpp.add_event_handler('blocked_message', self.on_blocked_message) + def check_saslexternal(self, features): + if 'urn:xmpp:saslcert:1' in features: + self.register_command('certs', self.command_certs, + desc=_('List the fingerprints of certificates' + ' which can connect to your account.'), + shortdesc=_('List allowed client certs.')) + self.register_command('cert_add', self.command_cert_add, + desc=_('Add a client certificate to the authorized ones. ' + 'It must have an unique name and be contained in ' + 'a PEM file. [management] is a boolean indicating' + ' if a client connected using this certificate can' + ' manage the certificates itself.'), + shortdesc=_('Add a client certificate.'), + usage=' [management]') + self.register_command('cert_disable', self.command_cert_disable, + desc=_('Remove a certificate from the list ' + 'of allowed ones. Clients currently ' + 'using this certificate will not be ' + 'forcefully disconnected.'), + shortdesc=_('Disable a certificate'), + usage='') + self.register_command('cert_revoke', self.command_cert_revoke, + desc=_('Remove a certificate from the list ' + 'of allowed ones. Clients currently ' + 'using this certificate will be ' + 'forcefully disconnected.'), + shortdesc=_('Revoke a certificate'), + usage='') + self.register_command('cert_fetch', self.command_cert_fetch, + desc=_('Retrieve a certificate with its ' + 'name. It will be stored in .'), + shortdesc=_('Fetch a certificate'), + usage=' ') + + @command_args_parser.ignored + def command_certs(self): + """ + /certs + """ + def cb(iq): + if iq['type'] == 'error': + self.core.information(_('Unable to retrieve the certificate list.'), + _('Error')) + return + certs = [] + for item in iq['sasl_certs']['items']: + users = '\n'.join(item['users']) + certs.append((item['name'], users)) + + if not certs: + return self.core.information(_('No certificates found'), _('Info')) + msg = _('Certificates:\n') + msg += '\n'.join(((' %s%s' % (item[0] + (': ' if item[1] else ''), item[1])) for item in certs)) + self.core.information(msg, 'Info') + + self.core.xmpp.plugin['xep_0257'].get_certs(callback=cb, timeout=3) + + @command_args_parser.quoted(2, 1) + def command_cert_add(self, args): + """ + /cert_add [cert-management] + """ + if not args or len(args) < 2: + return self.core.command_help('cert_add') + def cb(iq): + if iq['type'] == 'error': + self.core.information(_('Unable to add the certificate.'), _('Error')) + else: + self.core.information(_('Certificate added.'), _('Info')) + + name = args[0] + + try: + with open(args[1]) as fd: + crt = fd.read() + crt = crt.replace(ssl.PEM_FOOTER, '').replace(ssl.PEM_HEADER, '').replace(' ', '').replace('\n', '') + except Exception as e: + self.core.information('Unable to read the certificate: %s' % e, 'Error') + return + + if len(args) > 2: + management = args[2] + if management: + management = management.lower() + if management not in ('false', '0'): + management = True + else: + management = False + else: + management = False + else: + management = True + + self.core.xmpp.plugin['xep_0257'].add_cert(name, crt, callback=cb, + allow_management=management) + + @command_args_parser.quoted(1) + def command_cert_disable(self, args): + """ + /cert_disable + """ + if not args: + return self.core.command_help('cert_disable') + def cb(iq): + if iq['type'] == 'error': + self.core.information(_('Unable to disable the certificate.'), _('Error')) + else: + self.core.information(_('Certificate disabled.'), _('Info')) + + name = args[0] + + self.core.xmpp.plugin['xep_0257'].disable_cert(name, callback=cb) + + @command_args_parser.quoted(1) + def command_cert_revoke(self, args): + """ + /cert_revoke + """ + if not args: + return self.core.command_help('cert_revoke') + def cb(iq): + if iq['type'] == 'error': + self.core.information(_('Unable to revoke the certificate.'), _('Error')) + else: + self.core.information(_('Certificate revoked.'), _('Info')) + + name = args[0] + + self.core.xmpp.plugin['xep_0257'].revoke_cert(name, callback=cb) + + + @command_args_parser.quoted(2) + def command_cert_fetch(self, args): + """ + /cert_fetch + """ + if not args or len(args) < 2: + return self.core.command_help('cert_fetch') + def cb(iq): + if iq['type'] == 'error': + self.core.information(_('Unable to fetch the certificate.'), + _('Error')) + return + + cert = None + for item in iq['sasl_certs']['items']: + if item['name'] == name: + cert = base64.b64decode(item['x509cert']) + break + + if not cert: + return self.core.information(_('Certificate not found.'), _('Info')) + + cert = ssl.DER_cert_to_PEM_cert(cert) + with open(path, 'w') as fd: + fd.write(cert) + + self.core.information(_('File stored at %s') % path, 'Info') + + name = args[0] + path = args[1] + + self.core.xmpp.plugin['xep_0257'].get_certs(callback=cb) + def on_blocked_message(self, message): """ When we try to send a message to a blocked contact -- cgit v1.2.3