Skip to content

Commit

Permalink
coretasks, irc: implement CertFP / SASL EXTERNAL authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
dgw committed Jun 11, 2021
1 parent e2d8439 commit 2bcd1ed
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 11 deletions.
13 changes: 11 additions & 2 deletions docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,15 @@ And here is an example of server-based authentication using SASL::
auth_password = SopelIsGreat! # your bot's password
auth_target = PLAIN # default sasl mechanism

Finally, here is how to enable CertFP once you have a certificate that meets
your IRC network's requirements::

[core]
client_cert_file = /path/to/cert.pem # your bot's client certificate
# some networks require SASL EXTERNAL for CertFP to work
auth_method = sasl # if required
auth_target = EXTERNAL # if required


Multi-stage
-----------
Expand Down Expand Up @@ -457,7 +466,7 @@ When :attr:`~CoreSection.server_auth_method` is defined the settings used are:
* :attr:`~CoreSection.server_auth_username`: account's username
* :attr:`~CoreSection.server_auth_password`: account's password
* :attr:`~CoreSection.server_auth_sasl_mech`: the SASL mechanism to use
(defaults to ``PLAIN``)
(defaults to ``PLAIN``; ``EXTERNAL`` is also available)

For example, this will use NickServ ``IDENTIFY`` command and SASL mechanism::

Expand All @@ -468,7 +477,7 @@ For example, this will use NickServ ``IDENTIFY`` command and SASL mechanism::
auth_password = SopelIsGreat! # your bot's password
auth_target = NickServ # default value

# server-based auth
# SASL auth
server_auth_method = sasl # select server-based auth
server_auth_username = BotAccount # your bot's username
server_auth_password = SopelIsGreat! # your bot's password
Expand Down
21 changes: 20 additions & 1 deletion sopel/config/core_section.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ class CoreSection(StaticSection):
* ``PLAIN`` if using the ``sasl`` :attr:`auth_method`
The nickname of the NickServ service, or the name of the desired SASL
mechanism, if :attr:`auth_method` is set to one of these methods. This value
mechanism, if :attr:`auth_method` is set to one of these methods. For
SASL, the ``EXTERNAL`` option is available in case the IRC network
requires it (e.g. for CertFP using :attr:`client_cert_file`). This value
is otherwise ignored.
See :ref:`Authentication`.
Expand Down Expand Up @@ -263,6 +265,18 @@ class CoreSection(StaticSection):
"""

client_cert_file = FilenameAttribute('client_cert_file')
"""Filesystem path to a certificate file for CertFP.
This is expected to be a ``.pem`` file containing both the certificate and
private key. Most networks that support CertFP will give instructions for
generating this, typically using OpenSSL.
Some networks may refer to this authentication method as SASL EXTERNAL.
.. versionadded:: 8.0
"""

commands_on_connect = ListAttribute('commands_on_connect')
"""A list of commands to send upon successful connection to the IRC server.
Expand Down Expand Up @@ -1068,7 +1082,12 @@ def homedir(self):
:default: ``PLAIN``
``EXTERNAL`` is also supported, e.g. for using :attr:`client_cert_file` to
authenticate via CertFP.
.. versionadded:: 7.0
.. versionchanged:: 8.0
Added support for SASL EXTERNAL mechanism.
"""

server_auth_username = ValidatedAttribute('server_auth_username')
Expand Down
41 changes: 34 additions & 7 deletions sopel/coretasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,6 @@ def receive_cap_ack_sasl(bot):
if not password:
return

mech = mech or 'PLAIN'
available_mechs = bot.server_capabilities.get('sasl', '')
available_mechs = available_mechs.split(',') if available_mechs else []

Expand Down Expand Up @@ -1025,22 +1024,46 @@ def auth_proceed(bot, trigger):
be ignored. If none is set, then this function does nothing.
"""
if trigger.args[0] != '+':
# How did we get here? I am not good with computer.
if bot.config.core.auth_method == 'sasl':
mech = bot.config.core.auth_target or 'PLAIN'
elif bot.config.core.server_auth_method == 'sasl':
mech = bot.config.core.server_auth_sasl_mech or 'PLAIN'
else:
return

if mech == 'EXTERNAL':
if trigger.args[0] != '+':
# not an expected response from the server; abort SASL
token = '*'
else:
token = '+'

bot.write(('AUTHENTICATE', token))
return
# Is this right?

if bot.config.core.auth_method == 'sasl':
sasl_username = bot.config.core.auth_username
sasl_password = bot.config.core.auth_password
elif bot.config.core.server_auth_method == 'sasl':
sasl_username = bot.config.core.server_auth_username
sasl_password = bot.config.core.server_auth_password
else:
# How did we get here? I am not good with computer
return

sasl_username = sasl_username or bot.nick
sasl_token = _make_sasl_plain_token(sasl_username, sasl_password)
LOGGER.info("Sending SASL Auth token.")
send_authenticate(bot, sasl_token)

if mech == 'PLAIN':
if trigger.args[0] != '+':
# not an expected response from the server; abort SASL
token = '*'
else:
sasl_token = _make_sasl_plain_token(sasl_username, sasl_password)
LOGGER.info("Sending SASL Auth token.")
send_authenticate(bot, sasl_token)
return

# TODO: Implement SCRAM challenges


def _make_sasl_plain_token(account, password):
Expand Down Expand Up @@ -1127,12 +1150,16 @@ def sasl_mechs(bot, trigger):
def _get_sasl_pass_and_mech(bot):
password = None
mech = None

if bot.config.core.auth_method == 'sasl':
password = bot.config.core.auth_password
mech = bot.config.core.auth_target
elif bot.config.core.server_auth_method == 'sasl':
password = bot.config.core.server_auth_password
mech = bot.config.core.server_auth_sasl_mech

mech = 'PLAIN' if mech is None else mech.upper()

return password, mech


Expand Down
2 changes: 2 additions & 0 deletions sopel/irc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ def get_irc_backend(self):
if has_ssl:
backend_class = SSLAsynchatBackend
backend_kwargs.update({
'certfile': self.settings.core.client_cert_file,
'keyfile': self.settings.core.client_cert_file,
'verify_ssl': self.settings.core.verify_ssl,
'ca_certs': self.settings.core.ca_certs,
})
Expand Down
11 changes: 10 additions & 1 deletion sopel/irc/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,17 @@ class SSLAsynchatBackend(AsynchatBackend):
(default ``True``, for good reason)
:param str ca_certs: filesystem path to a CA Certs file containing trusted
root certificates
:param str certfile: filesystem path to a certificate for SSL/TLS client
authentication (CertFP)
:param str keyfile: filesystem path to the private key for ``certfile``
"""
def __init__(self, bot, verify_ssl=True, ca_certs=None, **kwargs):
def __init__(self, bot, verify_ssl=True, ca_certs=None, certfile=None, keyfile=None, **kwargs):
AsynchatBackend.__init__(self, bot, **kwargs)
self.verify_ssl = verify_ssl
self.ssl = None
self.ca_certs = ca_certs
self.certfile = certfile
self.keyfile = keyfile

def handle_connect(self):
"""Handle potential TLS connection."""
Expand All @@ -278,10 +283,14 @@ def handle_connect(self):
# version(s) it supports.
if not self.verify_ssl:
self.ssl = ssl.wrap_socket(self.socket, # lgtm [py/insecure-default-protocol]
certfile=self.certfile,
keyfile=self.keyfile,
do_handshake_on_connect=True,
suppress_ragged_eofs=True)
else:
self.ssl = ssl.wrap_socket(self.socket, # lgtm [py/insecure-default-protocol]
certfile=self.certfile,
keyfile=self.keyfile,
do_handshake_on_connect=True,
suppress_ragged_eofs=True,
cert_reqs=ssl.CERT_REQUIRED,
Expand Down

0 comments on commit 2bcd1ed

Please sign in to comment.