Fix #2847 (SASL External support)

- 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
This commit is contained in:
mathieui 2014-12-11 22:28:44 +01:00
parent 21d8a3e7e1
commit 00396c158a
No known key found for this signature in database
GPG key ID: C59F84CEEFD616E3
9 changed files with 293 additions and 5 deletions

View file

@ -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 =

View file

@ -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.

View file

@ -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]``

View file

@ -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

View file

@ -7,6 +7,7 @@ Contents:
:maxdepth: 2
carbons
client_certs
correct
personal_events
pyenv

View file

@ -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,

View file

@ -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')

View file

@ -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()

View file

@ -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