Skip to content

Commit b724c37

Browse files
committed
coretasks, irc: implement CertFP / SASL EXTERNAL authentication
1 parent e2d8439 commit b724c37

File tree

4 files changed

+59
-8
lines changed

4 files changed

+59
-8
lines changed

sopel/config/core_section.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,16 @@ def homedir(self):
10481048
silently from the triggering IRC user's perspective.
10491049
"""
10501050

1051+
sasl_cert_file = FilenameAttribute('sasl_cert_file')
1052+
"""Filesystem path to a certificate file for CertFP.
1053+
1054+
This is expected to be a ``.pem`` file containing both the certificate and
1055+
private key. Most networks that support CertFP will give instructions for
1056+
generating this, typically using OpenSSL.
1057+
1058+
Some networks may refer to this authentication method as SASL EXTERNAL.
1059+
"""
1060+
10511061
server_auth_method = ChoiceAttribute('server_auth_method',
10521062
choices=['sasl', 'server'])
10531063
"""The server authentication method.

sopel/coretasks.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -957,7 +957,6 @@ def receive_cap_ack_sasl(bot):
957957
if not password:
958958
return
959959

960-
mech = mech or 'PLAIN'
961960
available_mechs = bot.server_capabilities.get('sasl', '')
962961
available_mechs = available_mechs.split(',') if available_mechs else []
963962

@@ -1025,22 +1024,46 @@ def auth_proceed(bot, trigger):
10251024
be ignored. If none is set, then this function does nothing.
10261025
10271026
"""
1028-
if trigger.args[0] != '+':
1029-
# How did we get here? I am not good with computer.
1027+
if bot.config.core.auth_method == 'sasl':
1028+
mech = bot.config.core.auth_target or 'PLAIN'
1029+
elif bot.config.core.server_auth_method == 'sasl':
1030+
mech = bot.config.core.server_auth_sasl_mech or 'PLAIN'
1031+
else:
10301032
return
1031-
# Is this right?
1033+
1034+
if mech == 'EXTERNAL':
1035+
if trigger.args[0] != '+':
1036+
# not an expected response from the server; abort SASL
1037+
token = '*'
1038+
else:
1039+
token = '+'
1040+
1041+
bot.write(('AUTHENTICATE', token))
1042+
return
1043+
10321044
if bot.config.core.auth_method == 'sasl':
10331045
sasl_username = bot.config.core.auth_username
10341046
sasl_password = bot.config.core.auth_password
10351047
elif bot.config.core.server_auth_method == 'sasl':
10361048
sasl_username = bot.config.core.server_auth_username
10371049
sasl_password = bot.config.core.server_auth_password
10381050
else:
1051+
# How did we get here? I am not good with computer
10391052
return
1053+
10401054
sasl_username = sasl_username or bot.nick
1041-
sasl_token = _make_sasl_plain_token(sasl_username, sasl_password)
1042-
LOGGER.info("Sending SASL Auth token.")
1043-
send_authenticate(bot, sasl_token)
1055+
1056+
if mech == 'PLAIN':
1057+
if trigger.args[0] != '+':
1058+
# not an expected response from the server; abort SASL
1059+
token = '*'
1060+
else:
1061+
sasl_token = _make_sasl_plain_token(sasl_username, sasl_password)
1062+
LOGGER.info("Sending SASL Auth token.")
1063+
send_authenticate(bot, sasl_token)
1064+
return
1065+
1066+
# TODO: Implement SCRAM challenges
10441067

10451068

10461069
def _make_sasl_plain_token(account, password):
@@ -1127,12 +1150,19 @@ def sasl_mechs(bot, trigger):
11271150
def _get_sasl_pass_and_mech(bot):
11281151
password = None
11291152
mech = None
1153+
11301154
if bot.config.core.auth_method == 'sasl':
11311155
password = bot.config.core.auth_password
11321156
mech = bot.config.core.auth_target
11331157
elif bot.config.core.server_auth_method == 'sasl':
11341158
password = bot.config.core.server_auth_password
11351159
mech = bot.config.core.server_auth_sasl_mech
1160+
1161+
if mech is None:
1162+
mech = 'PLAIN'
1163+
else:
1164+
mech = mech.upper()
1165+
11361166
return password, mech
11371167

11381168

sopel/irc/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ def get_irc_backend(self):
153153
if has_ssl:
154154
backend_class = SSLAsynchatBackend
155155
backend_kwargs.update({
156+
'certfile': self.settings.core.sasl_cert_file,
157+
'keyfile': self.settings.core.sasl_cert_file,
156158
'verify_ssl': self.settings.core.verify_ssl,
157159
'ca_certs': self.settings.core.ca_certs,
158160
})

sopel/irc/backends.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,12 +260,17 @@ class SSLAsynchatBackend(AsynchatBackend):
260260
(default ``True``, for good reason)
261261
:param str ca_certs: filesystem path to a CA Certs file containing trusted
262262
root certificates
263+
:param str certfile: filesystem path to a certificate for SASL EXTERNAL
264+
authentication (CERTFP)
265+
:param str keyfile: filesystem path to the private key for ``certfile``
263266
"""
264-
def __init__(self, bot, verify_ssl=True, ca_certs=None, **kwargs):
267+
def __init__(self, bot, verify_ssl=True, ca_certs=None, certfile=None, keyfile=None, **kwargs):
265268
AsynchatBackend.__init__(self, bot, **kwargs)
266269
self.verify_ssl = verify_ssl
267270
self.ssl = None
268271
self.ca_certs = ca_certs
272+
self.certfile = certfile
273+
self.keyfile = keyfile
269274

270275
def handle_connect(self):
271276
"""Handle potential TLS connection."""
@@ -278,10 +283,14 @@ def handle_connect(self):
278283
# version(s) it supports.
279284
if not self.verify_ssl:
280285
self.ssl = ssl.wrap_socket(self.socket, # lgtm [py/insecure-default-protocol]
286+
certfile=self.certfile,
287+
keyfile=self.keyfile,
281288
do_handshake_on_connect=True,
282289
suppress_ragged_eofs=True)
283290
else:
284291
self.ssl = ssl.wrap_socket(self.socket, # lgtm [py/insecure-default-protocol]
292+
certfile=self.certfile,
293+
keyfile=self.keyfile,
285294
do_handshake_on_connect=True,
286295
suppress_ragged_eofs=True,
287296
cert_reqs=ssl.CERT_REQUIRED,

0 commit comments

Comments
 (0)