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:
parent
21d8a3e7e1
commit
00396c158a
9 changed files with 293 additions and 5 deletions
|
@ -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 =
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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]``
|
||||
|
|
43
doc/source/misc/client_certs.rst
Normal file
43
doc/source/misc/client_certs.rst
Normal 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
|
|
@ -7,6 +7,7 @@ Contents:
|
|||
:maxdepth: 2
|
||||
|
||||
carbons
|
||||
client_certs
|
||||
correct
|
||||
personal_events
|
||||
pyenv
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue