From 6baee708520fb8509aa2978a47606ce2157f3034 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 11 Jan 2017 00:07:25 +0100 Subject: [PATCH] Add tests for SCRAM. --- irctest/authentication.py | 2 + irctest/client_tests/test_sasl.py | 70 +++++++++++++++++++++++++++++++ irctest/controllers/limnoria.py | 3 +- requirements.txt | 1 + 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/irctest/authentication.py b/irctest/authentication.py index 2b8fb15..1ae39ea 100644 --- a/irctest/authentication.py +++ b/irctest/authentication.py @@ -8,9 +8,11 @@ class Mechanisms(enum.Enum): def as_string(cls, mech): return {cls.plain: 'PLAIN', cls.ecdsa_nist256p_challenge: 'ECDSA-NIST256P-CHALLENGE', + cls.scram_sha_256: 'SCRAM-SHA-256', }[mech] plain = 1 ecdsa_nist256p_challenge = 2 + scram_sha_256 = 3 Authentication = collections.namedtuple('Authentication', 'mechanisms username password ecdsa_key') diff --git a/irctest/client_tests/test_sasl.py b/irctest/client_tests/test_sasl.py index ca2bec3..b0fa550 100644 --- a/irctest/client_tests/test_sasl.py +++ b/irctest/client_tests/test_sasl.py @@ -1,5 +1,7 @@ import ecdsa import base64 +import pyxmpp2_scram as scram + from irctest import cases from irctest import authentication from irctest.irc_utils.message_parser import Message @@ -153,6 +155,74 @@ class SaslTestCase(cases.BaseClientTestCase, cases.ClientNegociationHelper, m = self.negotiateCapabilities(['sasl'], False) self.assertEqual(m, Message([], None, 'CAP', ['END'])) + @cases.OptionalityHelper.skipUnlessHasMechanism('SCRAM-SHA-256') + def testScram(self): + """Test SCRAM-SHA-256 authentication. + """ + auth = authentication.Authentication( + mechanisms=[authentication.Mechanisms.scram_sha_256], + username='jilles', + password='sesame', + ) + class PasswdDb: + def get_password(self, *args): + return ('sesame', 'plain') + authenticator = scram.SCRAMServerAuthenticator('SHA-256', + channel_binding=False, password_database=PasswdDb()) + + m = self.negotiateCapabilities(['sasl'], auth=auth) + self.assertEqual(m, Message([], None, 'AUTHENTICATE', ['SCRAM-SHA-256'])) + self.sendLine('AUTHENTICATE +') + + m = self.getMessage() + self.assertEqual(m.command, 'AUTHENTICATE', m) + client_first = base64.b64decode(m.params[0]) + response = authenticator.start(properties={}, initial_response=client_first) + assert isinstance(response, bytes), response + self.sendLine('AUTHENTICATE :' + base64.b64encode(response).decode()) + + m = self.getMessage() + self.assertEqual(m.command, 'AUTHENTICATE', m) + msg = base64.b64decode(m.params[0]) + r = authenticator.response(msg) + assert isinstance(r, tuple), r + assert len(r) == 2, r + (properties, response) = r + self.sendLine('AUTHENTICATE :' + base64.b64encode(response).decode()) + self.assertEqual(properties, {'authzid': None, 'username': 'jilles'}) + + @cases.OptionalityHelper.skipUnlessHasMechanism('SCRAM-SHA-256') + def testScramBadPassword(self): + """Test SCRAM-SHA-256 authentication with a bad password. + """ + auth = authentication.Authentication( + mechanisms=[authentication.Mechanisms.scram_sha_256], + username='jilles', + password='sesame', + ) + class PasswdDb: + def get_password(self, *args): + return ('notsesame', 'plain') + authenticator = scram.SCRAMServerAuthenticator('SHA-256', + channel_binding=False, password_database=PasswdDb()) + + m = self.negotiateCapabilities(['sasl'], auth=auth) + self.assertEqual(m, Message([], None, 'AUTHENTICATE', ['SCRAM-SHA-256'])) + self.sendLine('AUTHENTICATE +') + + m = self.getMessage() + self.assertEqual(m.command, 'AUTHENTICATE', m) + client_first = base64.b64decode(m.params[0]) + response = authenticator.start(properties={}, initial_response=client_first) + assert isinstance(response, bytes), response + self.sendLine('AUTHENTICATE :' + base64.b64encode(response).decode()) + + m = self.getMessage() + self.assertEqual(m.command, 'AUTHENTICATE', m) + msg = base64.b64decode(m.params[0]) + with self.assertRaises(scram.NotAuthorizedException): + authenticator.response(msg) + class Irc302SaslTestCase(cases.BaseClientTestCase, cases.ClientNegociationHelper, cases.OptionalityHelper): @cases.OptionalityHelper.skipUnlessHasMechanism('PLAIN') diff --git a/irctest/controllers/limnoria.py b/irctest/controllers/limnoria.py index 21b106f..848ea75 100644 --- a/irctest/controllers/limnoria.py +++ b/irctest/controllers/limnoria.py @@ -9,6 +9,7 @@ TEMPLATE_CONFIG = """ supybot.directories.conf: {directory}/conf supybot.directories.data: {directory}/data supybot.directories.migrations: {directory}/migrations +supybot.log.level: DEBUG supybot.log.stdout.level: {loglevel} supybot.networks: testnet @@ -27,7 +28,7 @@ supybot.networks.testnet.sasl.mechanisms: {mechanisms} class LimnoriaController(BaseClientController, DirectoryBasedController): software_name = 'Limnoria' supported_sasl_mechanisms = { - 'PLAIN', 'ECDSA-NIST256P-CHALLENGE', 'EXTERNAL', + 'PLAIN', 'ECDSA-NIST256P-CHALLENGE', 'SCRAM-SHA-256', 'EXTERNAL', } def create_config(self): super().create_config() diff --git a/requirements.txt b/requirements.txt index 1a3ed2a..d18fa09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ limnoria > 2012.08.04 # Needs MultipleReplacer, from 1a64f105 psutil >= 3.1.0 # Fixes #640 ecdsa +pyxmpp2_scram