Merge branch 'sasl-scram' into testing

This commit is contained in:
Valentin Lorentz 2017-01-11 00:11:26 +01:00
commit 260a511942
3 changed files with 86 additions and 15 deletions

View File

@ -312,7 +312,8 @@ class SpaceSeparatedSetOfChannels(registry.SpaceSeparatedListOf):
return None
class ValidSaslMechanism(registry.OnlySomeStrings):
validStrings = ('ecdsa-nist256p-challenge', 'external', 'plain')
validStrings = ('ecdsa-nist256p-challenge', 'external', 'plain',
'scram-sha-256')
class SpaceSeparatedListOfSaslMechanisms(registry.SpaceSeparatedListOf):
Value = ValidSaslMechanism

View File

@ -35,10 +35,14 @@ import base64
import collections
try:
from ecdsa import SigningKey, BadDigestError
ecdsa = True
import ecdsa
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 .utils.str import rsplit
@ -994,6 +998,7 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
self.sasl_username = network_config.sasl.username()
self.sasl_password = network_config.sasl.password()
self.sasl_ecdsa_key = network_config.sasl.ecdsa_key()
self.sasl_scram_state = {'step': 'uninitialized'}
self.authenticate_decoder = None
self.sasl_next_mechanisms = []
self.sasl_current_mechanism = None
@ -1006,6 +1011,9 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
network_config.certfile() or
conf.supybot.protocols.irc.certfile()):
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 \
self.sasl_username and self.sasl_password:
self.sasl_next_mechanisms.append(mechanism)
@ -1101,20 +1109,30 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
mechanism = self.sasl_current_mechanism
if mechanism == 'ecdsa-nist256p-challenge':
if string == b'':
self.sendSaslString(self.sasl_username.encode('utf-8'))
return
self.doAuthenticateEcdsa(string)
elif mechanism == 'external':
self.sendSaslString(b'')
elif mechanism.startswith('scram-'):
step = self.sasl_scram_state['step']
try:
with open(self.sasl_ecdsa_key) as fd:
private_key = SigningKey.from_pem(fd.read())
authstring = private_key.sign(base64.b64decode(msg.args[0].encode()))
self.sendSaslString(authstring)
except (BadDigestError, OSError, ValueError):
if step == 'uninitialized':
log.debug('%s: starting SCRAM.',
self.network)
self.doAuthenticateScramFirst(mechanism)
elif step == 'first-sent':
log.debug('%s: received SCRAM challenge.',
self.network)
self.doAuthenticateScramChallenge(string)
elif step == 'final-sent':
log.debug('%s: finishing SCRAM.',
self.network)
self.doAuthenticateScramFinish(string)
else:
assert False
except scram.ScramException:
self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
args=('*',)))
self.tryNextSaslMechanism()
elif mechanism == 'external':
self.sendSaslString(b'')
elif mechanism == 'plain':
authstring = b'\0'.join([
self.sasl_username.encode('utf-8'),
@ -1123,6 +1141,58 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
])
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(string)
self.sendSaslString(authstring)
except (ecdsa.BadDigestError, OSError, ValueError):
self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
args=('*',)))
self.tryNextSaslMechanism()
def doAuthenticateScramFirst(self, mechanism):
"""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')]
hash_name = hash_name.upper()
if hash_name not in scram.HASH_FACTORIES:
log.debug('%s: SCRAM hash %r not supported, aborting.',
self.network, hash_name)
self.tryNextSaslMechanism()
return
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.sendSaslString(client_final)
self.sasl_scram_state['step'] = 'final-sent'
def doAuthenticateScramFinish(self, data):
try:
res = self.sasl_scram_state['authenticator'] \
.finish(data)
except scram.BadSuccessException as e:
log.warning('%s: SASL authentication failed with SCRAM error: %e',
self.network, e)
self.tryNextSaslMechanism()
else:
self.sasl_scram_state['step'] = 'authenticated'
def do903(self, msg):
log.info('%s: SASL authentication successful', self.network)
self.sasl_authenticated = True

View File

@ -508,7 +508,7 @@ class Probability(Float):
class String(Value):
"""Value is not a valid Python string."""
errormsg = _('Value is not a valid Python string, not %r.')
errormsg = _('Value should be a valid Python string, not %r.')
def set(self, s):
v = s
if not v: