summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/default_config.cfg10
-rw-r--r--doc/source/commands.rst43
-rw-r--r--doc/source/configuration.rst16
-rw-r--r--doc/source/misc/client_certs.rst43
-rw-r--r--doc/source/misc/index.rst1
-rw-r--r--src/config.py2
-rw-r--r--src/connection.py16
-rw-r--r--src/core/handlers.py1
-rw-r--r--src/tabs/rostertab.py166
9 files changed, 293 insertions, 5 deletions
diff --git a/data/default_config.cfg b/data/default_config.cfg
index 39899d31..36ca7858 100644
--- a/data/default_config.cfg
+++ b/data/default_config.cfg
@@ -15,6 +15,16 @@ jid =
# If you leave this empty, the password will be asked at each startup
password =
+# Path to a PEM certificate file to use for certificate authentication
+# through SASL External. If set, keyfile MUST be provided as well in
+# order to login.
+certfile =
+
+# Path to a PEM private key file to use for certificate authentication
+# through SASL External. If set, certfile MUST be provided as well in
+# order to login.
+keyfile =
+
# the nick you will use when joining a room with no associated nick
# If this is empty, the $USER environnement variable will be used
default_nick =
diff --git a/doc/source/commands.rst b/doc/source/commands.rst
index 395b396b..f8f2b5e1 100644
--- a/doc/source/commands.rst
+++ b/doc/source/commands.rst
@@ -312,10 +312,10 @@ MultiUserChat tab commands
.. glossary::
:sorted:
- /clear [RosterTab version]
+ /clear [MUCTab version]
**Usage:** ``/clear``
- Clear the information buffer. (was /clear_infos)
+ Clear the messages buffer.
/ignore
**Usage:** ``/ignore <nickname>``
@@ -502,8 +502,8 @@ Roster tab commands
Disconnect from the remote server (if connected) and then
connect to it again.
-.. note:: The following commands only exist if your server supports them. If it
- does not, you will be notified when you start poezio.
+.. note:: The following commands only exist if your server announces it
+ supports them.
.. glossary::
:sorted:
@@ -523,6 +523,41 @@ Roster tab commands
/list_blocks
List the blocked JIDs.
+ /certs
+
+ List the remotely stored X.509 certificated allowed to connect
+ to your accounts.
+
+ /cert_add
+ **Usage:** ``/cert_add <name> <certificate file> [management]``
+
+ Add a client X.509 certificate to the list of the certificates
+ which grand access to your account. It must have an unique name
+ the file must be in PEM format. ``[management]`` is true by
+ default and specifies if the clients connecting with this
+ particular certificate will be able to manage the list of
+ authorized certificates.
+
+ /cert_disable
+ **Usage:** ``/cert_disable <name>``
+
+ Remove a certificate from the authorized list. Clients currently
+ connected with the certificate identified by ``<name>`` will
+ however **not** be disconnected.
+
+ /cert_revoke
+ **Usage:** ``/cert_revoke <name>``
+
+ Remove a certificate from the authorized list. Clients currently
+ connected with the certificate identified by ``<name>`` **will**
+ be disconnected.
+
+ /cert_fetch
+ **Usage:** ``/cert_fetch <name> <path>``
+
+ Download the public key of the authorized certificate identified by
+ ``name`` from the XMPP server, and store it in ``<path>``.
+
.. note:: The following commands do not comply with any XEP or whatever, but they
can still prove useful when you are migrating to an other JID.
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index b32cbec3..b7099020 100644
--- a/doc/source/configuration.rst
+++ b/doc/source/configuration.rst
@@ -156,6 +156,22 @@ Options related to account configuration, nicknameā€¦
your alternative nickname will be "john\_".
+ keyfile
+
+ **Default value:** ``[empty]``
+
+ Path to a PEM private key file to use for certificate authentication
+ through SASL External. If set, :term:`certfile` **MUST** be set as well
+ in order to login.
+
+ certfile
+
+ **Default value:** ``[empty]``
+
+ Path to a PEM certificate file to use for certificate authentication
+ through SASL External. If set, :term:`keyfile` **MUST** be set as well
+ in order to login.
+
resource
**Default value:** ``[empty]``
diff --git a/doc/source/misc/client_certs.rst b/doc/source/misc/client_certs.rst
new file mode 100644
index 00000000..df09ea3c
--- /dev/null
+++ b/doc/source/misc/client_certs.rst
@@ -0,0 +1,43 @@
+Using client certificates to login
+==================================
+
+Passwordless authentication is possible in XMPP through the use of mecanisms
+such as `SASL External`_. This mechanism has to be supported by both the client
+and the server. This page does not cover the server setup, but prosody has a
+`mod_client_certs`_ module which can perform this kind of authentication, and
+also helps you create a self-signed certificate.
+
+Poezio configuration
+--------------------
+
+If you created a certificate using the above link, you should have at least
+two files, a ``.crt`` (public key in PEM format) and a ``.key`` (private key
+in PEM format).
+
+You only have to store the files wherever you want and set :term:`keyfile`
+with the path to the private key (``.key``), and :term:`certfile` with the
+path to the public key (``.crt``).
+
+Authorizing your keys
+---------------------
+
+Now your poezio is setup to try to use client certificates at each connection.
+However, you still need to inform your XMPP server that you want to allow
+those keys to access your account.
+
+This is done through :term:`/cert_add`. Once you have added your certificate,
+you can try to connect without a password by commenting the option.
+
+.. note:: The :term:`/cert_add` command and the others are only available if
+ your server supports them.
+
+Next
+----
+Now that this is setup, you might want to use :term:`/certs` to list the
+keys currently known by your XMPP server, :term:`/cert_revoke` or
+:term:`/cert_disable` to remove them, and :term:`/cert_fetch` to retrieve
+a public key.
+
+
+.. _SASL External: http://xmpp.org/extensions/xep-0178.html
+.. _mod_client_certs: https://code.google.com/p/prosody-modules/wiki/mod_client_certs
diff --git a/doc/source/misc/index.rst b/doc/source/misc/index.rst
index fe8f1100..2603298e 100644
--- a/doc/source/misc/index.rst
+++ b/doc/source/misc/index.rst
@@ -7,6 +7,7 @@ Contents:
:maxdepth: 2
carbons
+ client_certs
correct
personal_events
pyenv
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='<name> <certificate path> [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='<name>')
+ 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='<name>')
+ self.register_command('cert_fetch', self.command_cert_fetch,
+ desc=_('Retrieve a certificate with its '
+ 'name. It will be stored in <path>.'),
+ shortdesc=_('Fetch a certificate'),
+ usage='<name> <path>')
+
+ @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 <name> <certfile> [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 <name>
+ """
+ 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 <name>
+ """
+ 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 <name> <path>
+ """
+ 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