changeset 1f3eb954483c in /home/hg/repos/python-nbxmpp
details:http://hg.gajim.org/python-nbxmpp?cmd=changeset;node=1f3eb954483c description: SCRAM-SHA-1-PLUS authentication mechanism Fixes #16
diffstat:
nbxmpp/auth_nb.py | 38 ++++++++++++++++++++++++++++---------- nbxmpp/client_nb.py | 10 +++++++++- nbxmpp/tls_nb.py | 10 ++++++++++ 3 files changed, 47 insertions(+), 11 deletions(-)
diffs (152 lines):
diff -r b92ebcc55cb4 -r 1f3eb954483c nbxmpp/auth_nb.py --- a/nbxmpp/auth_nb.py Tue Feb 25 16:36:56 2014 +0100 +++ b/nbxmpp/auth_nb.py Wed Feb 26 10:18:08 2014 +0100 @@ -88,7 +88,7 @@ return dict_
def _scram_parse(chatter): - """Helper function. Used for SCRAM-SHA-1 authentication""" + """Helper function. Used for SCRAM-SHA-1, SCRAM-SHA-1-PLUS authentication""" return dict(s.split('=', 1) for s in chatter.split(','))
class SASL(PlugIn): @@ -97,16 +97,19 @@ to start authentication """
- def __init__(self, username, password, on_sasl): + def __init__(self, username, password, on_sasl, channel_binding): """ :param username: XMPP username :param password: XMPP password :param on_sasl: Callback, will be called after each SASL auth-step. + :param channel_binding: TLS channel binding data, None if the + binding data is not available """ PlugIn.__init__(self) self.username = username self.password = password self.on_sasl = on_sasl + self.channel_binding = channel_binding self.realm = None
def plugin(self, owner): @@ -213,6 +216,13 @@ raise NodeProcessed except kerberos.GSSError, e: log.info('GSSAPI authentication failed: %s' % str(e)) + if 'SCRAM-SHA-1-PLUS' in self.mecs and self.channel_binding != None: + self.mecs.remove('SCRAM-SHA-1-PLUS') + self.mechanism = 'SCRAM-SHA-1-PLUS' + self._owner._caller.get_password(self.set_password, self.mechanism) + self.scram_step = 0 + self.startsasl = SASL_IN_PROCESS + raise NodeProcessed if 'SCRAM-SHA-1' in self.mecs: self.mecs.remove('SCRAM-SHA-1') self.mechanism = 'SCRAM-SHA-1' @@ -241,8 +251,9 @@ self.startsasl = SASL_IN_PROCESS raise NodeProcessed self.startsasl = SASL_FAILURE - log.info('I can only use EXTERNAL, SCRAM-SHA-1, DIGEST-MD5, GSSAPI and ' - 'PLAIN mecanisms.') + log.info('I can only use ANONYMOUS, EXTERNAL, GSSAPI, SCRAM-SHA-1-PLUS,' + ' SCRAM-SHA-1, DIGEST-MD5, PLAIN and X-MESSENGER-OAUTH2' + ' mechanisms.') if self.on_sasl: self.on_sasl() return @@ -283,7 +294,7 @@ reason = challenge on_auth_fail(reason) elif challenge.getName() == 'success': - if self.mechanism == 'SCRAM-SHA-1': + if self.mechanism in ('SCRAM-SHA-1', 'SCRAM-SHA-1-PLUS'): # check data-with-success data = _scram_parse(data) if data['v'] != scram_base64(self.scram_ServerSignature): @@ -327,7 +338,7 @@ self._owner.send(Node('response', attrs={'xmlns': NS_SASL}, payload=response).__str__()) raise NodeProcessed - if self.mechanism == 'SCRAM-SHA-1': + if self.mechanism in ('SCRAM-SHA-1', 'SCRAM-SHA-1-PLUS'): hashfn = hashlib.sha1
def HMAC(k, s): @@ -363,8 +374,12 @@ if (data['r'][:len(self.client_nonce)] != self.client_nonce): on_auth_fail('Server nonce is incorrect') raise NodeProcessed - # TODO: Channel binding data goes in here too. - r = 'c=' + scram_base64(self.scram_gs2) + if self.mechanism == 'SCRAM-SHA-1': + r = 'c=' + scram_base64(self.scram_gs2) + else: + # Channel binding data goes in here too. + r = 'c=' + scram_base64(self.scram_gs2 + + self.channel_binding) r += ',r=' + data['r'] self.scram_soup += r salt = data['s'].decode('base64') @@ -437,10 +452,13 @@
def set_password(self, password): self.password = '' if password is None else password - if self.mechanism == 'SCRAM-SHA-1': + if self.mechanism in ('SCRAM-SHA-1', 'SCRAM-SHA-1-PLUS'): self.client_nonce = '%x' % rndg.getrandbits(196) self.scram_soup = 'n=' + self.username + ',r=' + self.client_nonce - self.scram_gs2 = 'n,,' # No CB yet. + if self.mechanism == 'SCRAM-SHA-1': + self.scram_gs2 = 'n,,' # No CB. + else: + self.scram_gs2 = 'p=tls-unique,,' sasl_data = (self.scram_gs2 + self.scram_soup).encode('base64').\ replace('\n', '') node = Node('auth', attrs={'xmlns': NS_SASL, diff -r b92ebcc55cb4 -r 1f3eb954483c nbxmpp/client_nb.py --- a/nbxmpp/client_nb.py Tue Feb 25 16:36:56 2014 +0100 +++ b/nbxmpp/client_nb.py Wed Feb 26 10:18:08 2014 +0100 @@ -486,6 +486,14 @@ """ self._User, self._Password = user, password self._Resource, self._sasl = resource, sasl + self._channel_binding = None + if self.connected in ('ssl', 'tls'): + try: + self._channel_binding = self.Connection.NonBlockingTLS.get_channel_binding() + # TLS handshake is finished so channel binding data muss exist + assert (self._channel_binding != None) + except NotImplementedError: + pass self.on_auth = on_auth self._on_doc_attrs() return @@ -517,7 +525,7 @@ """ if self._sasl: auth_nb.SASL.get_instance(self._User, self._Password, - self._on_start_sasl).PlugIn(self) + self._on_start_sasl, self._channel_binding).PlugIn(self) if not hasattr(self, 'SASL'): return if not self._sasl or self.SASL.startsasl == 'not-supported': diff -r b92ebcc55cb4 -r 1f3eb954483c nbxmpp/tls_nb.py --- a/nbxmpp/tls_nb.py Tue Feb 25 16:36:56 2014 +0100 +++ b/nbxmpp/tls_nb.py Wed Feb 26 10:18:08 2014 +0100 @@ -478,3 +478,13 @@ log.error("Exception caught in _ssl_info_callback:", exc_info=True) # Make sure something is printed, even if log is disabled. traceback.print_exc() + + def get_channel_binding(self): + """ + Get channel binding data. RFC 5929 + """ + sslObj = self._owner._sslObj + try: + return sslObj.get_channel_binding() + except AttributeError: + raise NotImplementedError