def finish(self, data): """Process success indicator from the server. Process any addiitional data passed with the success. Fail if the server was not authenticated. :Parameters: - `data`: an optional additional data with success. :Types: - `data`: `bytes` :return: success or failure indicator. :returntype: `sasl.Success` or `sasl.Failure`""" if not self._server_first_message: logger.debug("Got success too early") return Failure("bad-success") if self._finished: return Success({ "username": self.username, "authzid": self.authzid }) else: ret = self._final_challenge(data) if isinstance(ret, Failure): return ret if self._finished: return Success({ "username": self.username, "authzid": self.authzid }) else: logger.debug("Something went wrong when processing additional" " data with success?") return Failure("bad-success")
def _get_realm(self, realms, charset): """Choose a realm from the list specified by the server. :Parameters: - `realms`: the realm list. - `charset`: encoding of realms on the list. :Types: - `realms`: `list` of `bytes` - `charset`: `bytes` :return: the realm chosen or a failure indicator. :returntype: `bytes` or `Failure`""" if realms: realm = realms[0] ap_realms = self.in_properties.get("realms") if ap_realms is not None: realms = (unicode(r, charset) for r in realms) for ap_realm in ap_realms: if ap_realm in realms: realm = ap_realm break realm = realm.decode(charset) else: realm = self.in_properties.get("realm") if realm is not None: self.realm = realm try: realm = realm.encode(charset) except UnicodeError: logger.debug( "Couldn't encode realm from utf-8 to {0!r}".format( charset)) return Failure("incompatible-charset") return realm
def _check_params(self, username, realm, cnonce, digest_uri, response_val, authzid, nonce_count): """Check parameters of a client reponse and pass them to further processing. :Parameters: - `username`: user name. - `realm`: realm. - `cnonce`: cnonce value. - `digest_uri`: digest-uri value. - `response_val`: response value computed by the client. - `authzid`: authorization id. - `nonce_count`: nonce count value. :Types: - `username`: `bytes` - `realm`: `bytes` - `cnonce`: `bytes` - `digest_uri`: `bytes` - `response_val`: `bytes` - `authzid`: `bytes` - `nonce_count`: `bytes` :return: a challenge, a success indicator or a failure indicator. :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`""" if not cnonce: logger.debug("Required 'cnonce' parameter not given") return Failure("not-authorized") if not response_val: logger.debug("Required 'response' parameter not given") return Failure("not-authorized") if not username: logger.debug("Required 'username' parameter not given") return Failure("not-authorized") if not digest_uri: logger.debug("Required 'digest_uri' parameter not given") return Failure("not-authorized") if not nonce_count: logger.debug("Required 'nc' parameter not given") return Failure("not-authorized") return self._make_final_challenge(username, realm, cnonce, digest_uri, response_val, authzid, nonce_count)
def challenge(self, challenge): """Process a challenge and return the response. :Parameters: - `challenge`: the challenge from server. :Types: - `challenge`: `bytes` :return: the response or a failure indicator. :returntype: `sasl.Response` or `sasl.Failure`""" if not challenge: logger.debug("Empty challenge") return Failure("bad-challenge") # workaround for some buggy implementations challenge = challenge.split(b'\x00')[0] if self.response_auth: return self._final_challenge(challenge) realms = [] nonce = None charset = "iso-8859-1" while challenge: match = PARAM_RE.match(challenge) if not match: logger.debug("Challenge syntax error: {0!r}".format(challenge)) return Failure("bad-challenge") challenge = match.group("rest") var = match.group("var") val = match.group("val") logger.debug("{0!r}: {1!r}".format(var, val)) if var == b"realm": realms.append(_unquote(val)) elif var == b"nonce": if nonce: logger.debug("Duplicate nonce") return Failure("bad-challenge") nonce = _unquote(val) elif var == b"qop": qopl = _unquote(val).split(b",") if b"auth" not in qopl: logger.debug("auth not supported") return Failure("not-implemented") elif var == b"charset": if val != b"utf-8": logger.debug("charset given and not utf-8") return Failure("bad-challenge") charset = "utf-8" elif var == b"algorithm": if val != b"md5-sess": logger.debug("algorithm given and not md5-sess") return Failure("bad-challenge") if not nonce: logger.debug("nonce not given") return Failure("bad-challenge") return self._make_response(charset, realms, nonce)
def _final_challenge(self, challenge): """Process the second challenge from the server and return the response. :Parameters: - `challenge`: the challenge from server. :Types: - `challenge`: `bytes` :return: the response or a failure indicator. :returntype: `sasl.Response` or `sasl.Failure` """ if self._finished: return Failure("extra-challenge") match = SERVER_FINAL_MESSAGE_RE.match(challenge) if not match: logger.debug("Bad final message syntax: {0!r}".format(challenge)) return Failure("bad-challenge") error = match.group("error") if error: logger.debug("Server returned SCRAM error: {0!r}".format(error)) return Failure(u"scram-" + error.decode("utf-8")) verifier = match.group("verifier") if not verifier: logger.debug("No verifier value in the final message") return Failure("bad-succes") server_key = self.HMAC(self._salted_password, b"Server Key") server_signature = self.HMAC(server_key, self._auth_message) if server_signature != a2b_base64(verifier): logger.debug("Server verifier does not match") return Failure("bad-succes") self._finished = True return Response(None)
def response(self, response): """Process a client reponse. :Parameters: - `response`: the response from the client. :Types: - `response`: `bytes` :return: a challenge, a success indicator or a failure indicator. :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`""" if self.out_properties: return Success(self.out_properties) if not response: return Failure("not-authorized") return self._parse_response(response)
def _final_challenge(self, challenge): """Process the second challenge from the server and return the response. :Parameters: - `challenge`: the challenge from server. :Types: - `challenge`: `bytes` :return: the response or a failure indicator. :returntype: `sasl.Response` or `sasl.Failure` """ if self.rspauth_checked: return Failure("extra-challenge") challenge = challenge.split(b'\x00')[0] rspauth = None while challenge: match = PARAM_RE.match(challenge) if not match: logger.debug("Challenge syntax error: {0!r}".format(challenge)) return Failure("bad-challenge") challenge = match.group("rest") var = match.group("var") val = match.group("val") logger.debug("{0!r}: {1!r}".format(var, val)) if var == b"rspauth": rspauth = val if not rspauth: logger.debug("Final challenge without rspauth") return Failure("bad-success") if rspauth == self.response_auth: self.rspauth_checked = True return Response(None) else: logger.debug("Wrong rspauth value - peer is cheating?") logger.debug("my rspauth: {0!r}".format(self.response_auth)) return Failure("bad-success")
def challenge(self, challenge): """Process a challenge and return the response. :Parameters: - `challenge`: the challenge from server. :Types: - `challenge`: `bytes` :return: the response or a failure indicator. :returntype: `sasl.Response` or `sasl.Failure` """ if not challenge: logger.debug("Empty challenge") return Failure("bad-challenge") if self._server_first_message: return self._final_challenge(challenge) match = SERVER_FIRST_MESSAGE_RE.match(challenge) if not match: logger.debug("Bad challenge syntax: {0!r}".format(challenge)) return Failure("bad-challenge") self._server_first_message = challenge mext = match.group("mext") if mext: logger.debug("Unsupported extension received: {0!r}".format(mext)) return Failure("bad-challenge") nonce = match.group("nonce") if not nonce.startswith(self._c_nonce): logger.debug("Nonce does not start with our nonce") return Failure("bad-challenge") salt = match.group("salt") try: salt = a2b_base64(salt) except ValueError: logger.debug("Bad base64 encoding for salt: {0!r}".format(salt)) return Failure("bad-challenge") iteration_count = match.group("iteration_count") try: iteration_count = int(iteration_count) except ValueError: logger.debug("Bad iteration_count: {0!r}".format(iteration_count)) return Failure("bad-challenge") return self._make_response(nonce, salt, iteration_count)
def _handle_final_response(self, response): match = CLIENT_FINAL_MESSAGE_RE.match(response) if not match: logger.debug("Bad response syntax: {0!r}".format(response)) return Failure("not-authorized") if match.group("nonce") != self._nonce: logger.debug("Bad nonce in the final client response") return Failure("not-authorized") cb_input = a2b_base64(match.group("cb")) if not cb_input.startswith(self._gs2_header): logger.debug( "GS2 header in the final response ({0!r}) doesn't" " match the one sent in the first message ({1!r})".format( cb_input, self._gs2_header)) return Failure("not-authorized") if self._cb_name: cb_data = cb_input[len(self._gs2_header):] if cb_data != self.properties["channel-binding"][self._cb_name]: logger.debug("Channel binding data doesn't match") return Failure("not-authorized") proof = a2b_base64(match.group("proof")) auth_message = (self._client_first_message_bare + b"," + self._server_first_message + b"," + match.group("without_proof")) if self._stored_key is None: # compute something to prevent timing attack client_signature = self.HMAC(b"", auth_message) client_key = self.XOR(client_signature, proof) self.H(client_key) logger.debug("Authentication failed (bad username)") return Failure("not-authorized") client_signature = self.HMAC(self._stored_key, auth_message) client_key = self.XOR(client_signature, proof) if self.H(client_key) != self._stored_key: logger.debug("Authentication failed") return Failure("not-authorized") server_signature = self.HMAC(self._server_key, auth_message) server_final_message = b"v=" + standard_b64encode(server_signature) return Success(self.out_properties, server_final_message)
def _make_final_challenge(self, username, realm, cnonce, digest_uri, response_val, authzid, nonce_count): """Send the second challenge in reply to the client response. :Parameters: - `username`: user name. - `realm`: realm. - `cnonce`: cnonce value. - `digest_uri`: digest-uri value. - `response_val`: response value computed by the client. - `authzid`: authorization id. - `nonce_count`: nonce count value. :Types: - `username`: `bytes` - `realm`: `bytes` - `cnonce`: `bytes` - `digest_uri`: `bytes` - `response_val`: `bytes` - `authzid`: `bytes` - `nonce_count`: `bytes` :return: a challenge, a success indicator or a failure indicator. :returntype: `sasl.Success` or `sasl.Failure` """ username_uq = username.replace(b'\\', b'') if authzid: authzid_uq = authzid.replace(b'\\', b'') else: authzid_uq = None if realm: realm_uq = realm.replace(b'\\', b'') else: realm_uq = None digest_uri_uq = digest_uri.replace(b'\\', b'') props = dict(self.in_properties) props["realm"] = realm_uq.decode("utf-8") password, pformat = self.password_database.get_password( username_uq.decode("utf-8"), (u"plain", u"md5:user:realm:pass"), props) if pformat == u"md5:user:realm:pass": urp_hash = password.a2b_hex() elif pformat == u"plain": urp_hash = _make_urp_hash(username, realm, password.encode("utf-8")) else: logger.debug(u"Couldn't get password.") return Failure(u"not-authorized") valid_response = _compute_response(urp_hash, self.nonce, cnonce, nonce_count, authzid, digest_uri) if response_val != valid_response: logger.debug(u"Response mismatch: {0!r} != {1!r}".format( response_val, valid_response)) return Failure(u"not-authorized") try: fields = digest_uri_uq.split(b"/") if len(fields) == 3: serv_type, host, serv_name = [ f.decode("utf-8") for f in fields ] elif len(fields) == 2: serv_type, host = [f.decode("utf-8") for f in fields] serv_name = None else: raise ValueError except (ValueError, UnicodeError): logger.debug("Bad digest_uri: {0!r}".format(digest_uri_uq)) return Failure("not-authorized") if "service-type" in self.in_properties: if serv_type != self.in_properties["service-type"]: logger.debug(u"Bad serv-type: {0!r} != {1!r}".format( serv_type, self.in_properties["service-type"])) return Failure("not-authorized") if "service-domain" in self.in_properties: if serv_name: if serv_name != self.in_properties["service-domain"]: logger.debug(u"serv-name: {0!r} != {1!r}".format( serv_name, self.in_properties["service-domain"])) return Failure("not-authorized") elif (host != self.in_properties["service-domain"] and host != self.in_properties.get("service-hostname")): logger.debug(u"bad host: {0!r} != {1!r}" u" & {0!r} != {2!r}".format( host, self.in_properties["service-domain"], self.in_properties.get("service-hostname"))) return Failure("not-authorized") if "service-hostname" in self.in_properties: if host != self.in_properties["service-hostname"]: logger.debug(u"bad host: {0!r} != {1!r}".format( host, self.in_properties["service-hostname"])) return Failure("not-authorized") rspauth = _compute_response_auth(urp_hash, self.nonce, cnonce, nonce_count, authzid, digest_uri) if authzid_uq is not None: authzid_uq = authzid_uq.decode("utf-8") self.out_properties = { "username": username.decode("utf-8"), "realm": realm.decode("utf-8"), "authzid": authzid_uq, "service-type": serv_type, "service-domain": serv_name if serv_name else host, "service-hostname": host } return Success(self.out_properties, b"rspauth=" + rspauth)
def _parse_response(self, response): """Parse a client reponse and pass to further processing. :Parameters: - `response`: the response from the client. :Types: - `response`: `bytes` :return: a challenge, a success indicator or a failure indicator. :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`""" # workaround for some SASL implementations response = response.split(b'\x00')[0] if self.realm: realm = self.realm.encode("utf-8") realm = _quote(realm) else: realm = None username = None cnonce = None digest_uri = None response_val = None authzid = None nonce_count = None while response: match = PARAM_RE.match(response) if not match: logger.debug("Response syntax error: {0!r}".format(response)) return Failure("not-authorized") response = match.group("rest") var = match.group("var") val = match.group("val") logger.debug("{0!r}: {1!r}".format(var, val)) if var == b"realm": realm = val[1:-1] elif var == b"cnonce": if cnonce: logger.debug("Duplicate cnonce") return Failure("not-authorized") cnonce = val[1:-1] elif var == b"qop": if val != b'auth': logger.debug("qop other then 'auth'") return Failure("not-authorized") elif var == b"digest-uri": digest_uri = val[1:-1] elif var == b"authzid": authzid = val[1:-1] elif var == b"username": username = val[1:-1] elif var == b"response": response_val = val elif var == b"nc": nonce_count = val self.last_nonce_count += 1 if int(nonce_count) != self.last_nonce_count: logger.debug("bad nonce: {0!r} != {1!r}".format( nonce_count, self.last_nonce_count)) return Failure("not-authorized") return self._check_params(username, realm, cnonce, digest_uri, response_val, authzid, nonce_count)
def _make_response(self, charset, realms, nonce): """Make a response for the first challenge from the server. :Parameters: - `charset`: charset name from the challenge. - `realms`: realms list from the challenge. - `nonce`: nonce value from the challenge. :Types: - `charset`: `bytes` - `realms`: `bytes` - `nonce`: `bytes` :return: the response or a failure indicator. :returntype: `sasl.Response` or `sasl.Failure`""" params = [] realm = self._get_realm(realms, charset) if isinstance(realm, Failure): return realm elif realm: realm = _quote(realm) params.append(b'realm="' + realm + b'"') try: username = self.username.encode(charset) except UnicodeError: logger.debug("Couldn't encode username to {0!r}".format(charset)) return Failure("incompatible-charset") username = _quote(username) params.append(b'username="******"') cnonce = self.in_properties.get("nonce_factory", default_nonce_factory)() cnonce = _quote(cnonce) params.append(b'cnonce="' + cnonce + b'"') params.append(b'nonce="' + nonce + b'"') self.nonce_count += 1 nonce_count = "{0:08x}".format(self.nonce_count).encode("us-ascii") params.append(b'nc=' + nonce_count) params.append(b'qop=auth') serv_type = self.in_properties["service-type"] serv_type = serv_type.encode("us-ascii") serv_name = self.in_properties["service-domain"] host = self.in_properties.get("service-hostname", serv_name) serv_name = serv_name.encode("idna") host = host.encode("idna") if serv_name and serv_name != host: digest_uri = b"/".join((serv_type, host, serv_name)) else: digest_uri = b"/".join((serv_type, host)) digest_uri = _quote(digest_uri) params.append(b'digest-uri="' + digest_uri + b'"') if self.authzid: try: authzid = self.authzid.encode(charset) except UnicodeError: logger.debug( "Couldn't encode authzid to {0!r}".format(charset)) return Failure("incompatible-charset") authzid = _quote(authzid) else: authzid = b"" try: epasswd = self.in_properties["password"].encode(charset) except UnicodeError: logger.debug("Couldn't encode password to {0!r}".format(charset)) return Failure("incompatible-charset") logger.debug("Encoded password: {0!r}".format(epasswd)) urp_hash = _make_urp_hash(username, realm, epasswd) response = _compute_response(urp_hash, nonce, cnonce, nonce_count, authzid, digest_uri) self.response_auth = _compute_response_auth(urp_hash, nonce, cnonce, nonce_count, authzid, digest_uri) params.append(b'response=' + response) if authzid: params.append(b'authzid="' + authzid + b'"') return Response(b",".join(params))
def _handle_first_response(self, response): match = CLIENT_FIRST_MESSAGE_RE.match(response) if not match: logger.debug("Bad response syntax: {0!r}".format(response)) return Failure("not-authorized") mext = match.group("mext") if mext: logger.debug("Unsupported extension received: {0!r}".format(mext)) return Failure("not-authorized") gs2_header = match.group("gs2_header") cb_name = match.group("cb_name") if self.channel_binding: if not cb_name: logger.debug("{0!r} used with no channel-binding".format( self.name)) return Failure("not-authorized") cb_name = cb_name.decode("utf-8") if cb_name not in self.properties["channel-binding"]: logger.debug( "Channel binding data type {0!r} not available".format( cb_name)) return Failure("not-authorized") else: if gs2_header.startswith(b'y'): plus_name = self.name + "-PLUS" if plus_name in self.properties.get("enabled_mechanisms", []): logger.warning("Channel binding downgrade attack detected") return Failure("not-authorized") elif gs2_header.startswith(b'p'): # is this really an error? logger.debug("Channel binding requested for {0!r}".format( self.name)) return Failure("not-authorized") authzid = match.group("authzid") if authzid: self.out_properties['authzid'] = self.unescape(authzid).decode( "utf-8") else: self.out_properties['authzid'] = None username = self.unescape(match.group("username")).decode("utf-8") self.out_properties['username'] = username nonce_factory = self.properties.get("nonce_factory", default_nonce_factory) properties = dict(self.properties) properties.update(self.out_properties) s_pformat = "SCRAM-{0}-SaltedPassword".format(self.hash_function_name) k_pformat = "SCRAM-{0}-Keys".format(self.hash_function_name) password, pformat = self.password_database.get_password( username, (s_pformat, "plain"), properties) if pformat == s_pformat: if password is not None: salt, iteration_count, salted_password = password else: logger.debug("No password for user {0!r}".format(username)) elif pformat != k_pformat: salt = self.properties.get("SCRAM-salt") if not salt: salt = nonce_factory() iteration_count = self.properties.get("SCRAM-iteration-count", 4096) if pformat == "plain" and password is not None: salted_password = self.Hi(self.Normalize(password), salt, iteration_count) else: logger.debug("No password for user {0!r}".format(username)) password = None # to prevent timing attack, compute the key anyway salted_password = self.Hi(self.Normalize(""), salt, iteration_count) if pformat == k_pformat: salt, iteration_count, stored_key, server_key = password else: client_key = self.HMAC(salted_password, b"Client Key") stored_key = self.H(client_key) server_key = self.HMAC(salted_password, b"Server Key") if password is not None: self._stored_key = stored_key self._server_key = server_key else: self._stored_key = None self._server_key = None c_nonce = match.group("nonce") s_nonce = nonce_factory() if not VALUE_CHARS_RE.match(s_nonce): s_nonce = standard_b64encode(s_nonce) nonce = c_nonce + s_nonce server_first_message = (b"r=" + nonce + b",s=" + standard_b64encode(salt) + b",i=" + str(iteration_count).encode("utf-8")) self._nonce = nonce self._cb_name = cb_name self._gs2_header = gs2_header self._client_first_message_bare = match.group("client_first_bare") self._server_first_message = server_first_message return Challenge(server_first_message)