Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

coretasks, irc: implement CertFP / SASL EXTERNAL authentication #2100

Merged
merged 1 commit into from
Jul 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
dgw marked this conversation as resolved.
Show resolved Hide resolved
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