Untested implementation of SASL SCRAM auth.

This commit is contained in:
Valentin Lorentz 2016-09-10 20:16:28 +02:00
parent 90c5c78813
commit e87ad5b5a3

View File

@ -35,10 +35,14 @@ import base64
import collections import collections
try: try:
from ecdsa import SigningKey, BadDigestError import ecdsa
ecdsa = True
except ImportError: except ImportError:
ecdsa = False ecdsa = None
try:
import pyxmpp2_scram as scram
except ImportError:
scram = None
from . import conf, ircdb, ircmsgs, ircutils, log, utils, world from . import conf, ircdb, ircmsgs, ircutils, log, utils, world
from .utils.str import rsplit from .utils.str import rsplit
@ -994,6 +998,7 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
self.sasl_username = network_config.sasl.username() self.sasl_username = network_config.sasl.username()
self.sasl_password = network_config.sasl.password() self.sasl_password = network_config.sasl.password()
self.sasl_ecdsa_key = network_config.sasl.ecdsa_key() self.sasl_ecdsa_key = network_config.sasl.ecdsa_key()
self.sasl_scram_state = {'step': 'uninitialized'}
self.authenticate_decoder = None self.authenticate_decoder = None
self.sasl_next_mechanisms = [] self.sasl_next_mechanisms = []
self.sasl_current_mechanism = None self.sasl_current_mechanism = None
@ -1006,6 +1011,9 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
network_config.certfile() or network_config.certfile() or
conf.supybot.protocols.irc.certfile()): conf.supybot.protocols.irc.certfile()):
self.sasl_next_mechanisms.append(mechanism) self.sasl_next_mechanisms.append(mechanism)
elif mechanism.startswith('scram-') and scram and \
self.sasl_username and self.sasl_password:
self.sasl_next_mechanisms.append(mechanism)
elif mechanism == 'plain' and \ elif mechanism == 'plain' and \
self.sasl_username and self.sasl_password: self.sasl_username and self.sasl_password:
self.sasl_next_mechanisms.append(mechanism) self.sasl_next_mechanisms.append(mechanism)
@ -1101,20 +1109,24 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
mechanism = self.sasl_current_mechanism mechanism = self.sasl_current_mechanism
if mechanism == 'ecdsa-nist256p-challenge': if mechanism == 'ecdsa-nist256p-challenge':
if string == b'': self.doAuthenticateEcdsa(string)
self.sendSaslString(self.sasl_username.encode('utf-8')) elif mechanism == 'external':
return self.sendSaslString(b'')
elif mechanism.startswith('scram-'):
step = self.sasl_scram_state['step']
try: try:
with open(self.sasl_ecdsa_key) as fd: if step == 'uninitialized':
private_key = SigningKey.from_pem(fd.read()) self.doAuthenticateScramFirst()
authstring = private_key.sign(base64.b64decode(msg.args[0].encode())) elif step == 'first-sent':
self.sendSaslString(authstring) self.doAuthenticateScramChallenge(string)
except (BadDigestError, OSError, ValueError): elif step == 'final-sent':
self.doAuthenticateScramFinish(string)
else:
assert False
except scram.ScramException:
self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
args=('*',))) args=('*',)))
self.tryNextSaslMechanism() self.tryNextSaslMechanism()
elif mechanism == 'external':
self.sendSaslString(b'')
elif mechanism == 'plain': elif mechanism == 'plain':
authstring = b'\0'.join([ authstring = b'\0'.join([
self.sasl_username.encode('utf-8'), self.sasl_username.encode('utf-8'),
@ -1123,6 +1135,46 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
]) ])
self.sendSaslString(authstring) self.sendSaslString(authstring)
def doAuthenticateEcdsa(self, string):
if string == b'':
self.sendSaslString(self.sasl_username.encode('utf-8'))
return
try:
with open(self.sasl_ecdsa_key) as fd:
private_key = ecdsa.SigningKey.from_pem(fd.read())
authstring = private_key.sign(base64.b64decode(msg.args[0].encode()))
self.sendSaslString(authstring)
except (ecdsa.BadDigestError, OSError, ValueError):
self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
args=('*',)))
self.tryNextSaslMechanism()
def doAuthenticateScramFirst(self):
"""Handle sending the client-first message of SCRAM auth."""
hash_name = mechanism[len('scram-'):]
if hash_name.endswith('-plus'):
hash_name = hash_name[:-len('-plus')]
authenticator = scram.SCRAMClientAuthenticator(hash_name,
channel_binding=False)
self.sasl_scram_state['authenticator'] = authenticator
client_first = authenticator.start({
'username': self.sasl_username,
'password': self.sasl_password,
})
self.sendSaslString(client_first)
self.sasl_scram_state['step'] = 'first-sent'
def doAuthenticateScramChallenge(self, challenge):
client_final = self.sasl_scram_state['authenticator'] \
.challenge(challenge)
self.sasl_scram_state['step'] = 'final-sent'
def doAuthenticateScramFinish(self, data):
# TODO: do something with BadSuccessException
res = self.sasl_scram_state['authenticator'] \
.finish(data)
self.sasl_scram_state['step'] = 'authenticated'
def do903(self, msg): def do903(self, msg):
log.info('%s: SASL authentication successful', self.network) log.info('%s: SASL authentication successful', self.network)
self.sasl_authenticated = True self.sasl_authenticated = True