Example #1
0
    def finish_challenge_token(self):
        """
        processing of the challenge tokens
        """
        challenge_tokens = self.challenge_tokens
        options = self.options
        if not options:
            options = {}

        action_detail = 'challenge created'

        if len(challenge_tokens) == 1:
            challenge_token = challenge_tokens[0]
            _res, reply = Challenges.create_challenge(
                                challenge_token, options=options)
            return (False, reply, action_detail)

        # processing of multiple challenges
        else:
            # for each token, who can submit a challenge, we have to
            # create the challenge. To mark the challenges as depending
            # the transaction id will have an id that all sub transaction share
            # and a postfix with their enumaration. Finally the result is
            # composed by the top level transaction id and the message
            # and below in a dict for each token a challenge description -
            # the key is the token type combined with its token serial number
            all_reply = {'challenges': {}}
            challenge_count = 0
            transactionid = ''
            challenge_id = ""
            for challenge_token in challenge_tokens:
                challenge_count += 1
                id_postfix = ".%02d" % challenge_count
                if transactionid:
                    challenge_id = "%s%s" % (transactionid, id_postfix)

                (_res, reply) = Challenges.create_challenge(
                    challenge_token,
                    options=options,
                    challenge_id=challenge_id,
                    id_postfix=id_postfix
                )
                transactionid = reply.get('transactionid').rsplit('.')[0]

                # add token type and serial to ease the type specific processing
                reply['linotp_tokentype'] = challenge_token.type
                reply['linotp_tokenserial'] = challenge_token.getSerial()
                key = challenge_token.getSerial()
                all_reply['challenges'][key] = reply

            # finally add the root challenge response with top transaction id
            # and message, that indicates that 'multiple challenges have been
            # submitted
            all_reply['transactionid'] = transactionid
            all_reply['message'] = "Multiple challenges submitted."

            log.debug("Multiple challenges submitted: %d",
                      len(challenge_tokens))

            return (False, all_reply, action_detail)
Example #2
0
    def finish_challenge_token(self):
        """
        processing of the challenge tokens
        """
        challenge_tokens = self.challenge_tokens
        options = self.options
        if not options:
            options = {}

        action_detail = 'challenge created'

        if len(challenge_tokens) == 1:
            challenge_token = challenge_tokens[0]

            _res, reply = Challenges.create_challenge(challenge_token,
                                                      options=options)

            return (False, reply, action_detail)

        # processing of multiple challenges
        else:
            # for each token, who can submit a challenge, we have to
            # create the challenge. To mark the challenges as depending
            # the transaction id will have an id that all sub transaction share
            # and a postfix with their enumeration. Finally the result is
            # composed by the top level transaction id and the message
            # and below in a dict for each token a challenge description -
            # the key is the token type combined with its token serial number

            all_reply = {'challenges': {}}
            challenge_count = 0
            transactionid = ''
            challenge_id = ""
            for challenge_token in challenge_tokens:
                challenge_count += 1
                id_postfix = ".%02d" % challenge_count
                if transactionid:
                    challenge_id = "%s%s" % (transactionid, id_postfix)

                (_res, reply) = Challenges.create_challenge(
                    challenge_token,
                    options=options,
                    challenge_id=challenge_id,
                    id_postfix=id_postfix)
                transactionid = reply.get('transactionid').rsplit('.')[0]
                key = challenge_token.getSerial()
                all_reply['challenges'][key] = reply

            # finally add the root challenge response with top transaction id
            # and message, that indicates that 'multiple challenges have been
            # submitted
            all_reply['transactionid'] = transactionid
            all_reply['message'] = "Multiple challenges submitted."

            log.debug("Multiple challenges submitted: %d",
                      len(challenge_tokens))

            return (False, all_reply, action_detail)
Example #3
0
    def finish_valid_tokens(self):
        """
        processing of the valid tokens
        """
        valid_tokens = self.valid_tokens
        validation_results = self.validation_results
        user = self.user

        if len(valid_tokens) == 1:
            token = valid_tokens[0]
            if user:
                action_detail = ("user %r@%r successfully authenticated."
                                 % (user.login, user.realm))
            else:
                action_detail = ("serial %r successfully authenticated."
                                 % token.getSerial())

            log.info(action_detail)

            # there could be a match in the window ahead,
            # so we need the last valid counter here
            (counter, _reply) = validation_results[token.getSerial()]
            token.setOtpCount(counter + 1)
            token.statusValidationSuccess()
            # finish as well related open challenges
            Challenges.finish_challenges(token, success=True)

            if token.getFromTokenInfo('count_auth_success_max', default=None):
                auth_count = token.get_count_auth_success()
                token.set_count_auth_success(auth_count + 1)

            detail = None
            auth_info = self.options.get('auth_info', 'False')
            if auth_info.lower() == "true":
                detail = token.getAuthDetail()
            return (True, detail, action_detail)

        else:
            # we have to set the matching counter to prevent replay one one
            # single token
            for token in valid_tokens:
                (res, _reply) = validation_results[token.getSerial()]
                token.setOtpCount(res)

            context['audit']['action_detail'] = "Multiple valid tokens found!"
            if user:
                log.error("[__checkTokenList] multiple token match error: "
                          "Several Tokens matching with the same OTP PIN "
                          "and OTP for user %r. Not sure how to auth",
                          user.login)
            raise UserError("multiple token match error", id=-33)
Example #4
0
    def finish_pin_matching_tokens(self):
        """
            check, if there have been some tokens
            where the pin matched (but OTP failed
            and increment only these
        """
        pin_matching_tokens = self.pin_matching_tokens
        action_detail = "wrong otp value"

        for tok in pin_matching_tokens:
            tok.statusValidationFail()
            tok.inc_count_auth()
            Challenges.finish_challenges(tok, success=False)

        return (False, None, action_detail)
Example #5
0
    def finish_pin_matching_tokens(self):
        """
            check, if there have been some tokens
            where the pin matched (but OTP failed
            and increment only these
        """
        pin_matching_tokens = self.pin_matching_tokens
        action_detail = "wrong otp value"

        for tok in pin_matching_tokens:
            tok.statusValidationFail()
            tok.inc_count_auth()
            Challenges.finish_challenges(tok, success=False)

        return (False, None, action_detail)
Example #6
0
def get_transaction_detail(transactionid):
    """Provide the information about a transaction.

    :param transactionid: the transaction id
    :return: dict with detail about challenge status
    """

    _exp, challenges = Challenges.get_challenges(transid=transactionid)

    if not challenges:
        return {}

    challenge = challenges[0]

    challenge_session = challenge.getSession()
    if challenge_session:
        challenge_session = json.loads(challenge_session)
    else:
        challenge_session = {}

    details = {
        'received_count': challenge.received_count,
        'received_tan': challenge.received_tan,
        'valid_tan': challenge.valid_tan,
        'message': challenge.getChallenge(),
        'status': challenge.getStatus(),
        'accept': challenge_session.get('accept', False),
        'reject': challenge_session.get('reject', False),
    }

    return details
Example #7
0
    def checkResponse4Challenge(self, user, passw, options=None, challenges=None):
        '''
        verify the response of a previous challenge

        :param user:     the requesting user
        :param passw:    the to be checked pass (pin+otp)
        :param options:  options an additional argument, which could be token
                          specific
        :param challenges: the list of challenges, where each challenge is
                            described as dict
        :return: tuple of (otpcounter and the list of matching challenges)

        '''
        otp_counter = -1
        transid = None
        matching = None
        matchin_challenges = []

        if 'transactionid' in options or 'state' in options:
            ## fetch the transactionid
            transid = options.get('transactionid', options.get('state', None))

        # check if the transactionid is in the list of challenges
        if transid is not None:
            for challenge in challenges:
                if Challenges.is_same_transaction(challenge, transid):
                    matching = challenge
                    break
            if matching is not None:
                otp_counter = check_otp(self, passw, options=options)
                if otp_counter >= 0:
                    matchin_challenges.append(matching)

        return (otp_counter, matchin_challenges)
Example #8
0
    def checkResponse4Challenge(self,
                                user,
                                passw,
                                options=None,
                                challenges=None):
        """
        This method verifies if the given ``passw`` matches any existing ``challenge``
        of the token.

        It then returns the new otp_counter of the token and the
        list of the matching challenges.

        In case of success the otp_counter needs to be > 0.
        The matching_challenges is passed to the method
        :py:meth:`~linotp.tokens.base.TokenClass.challenge_janitor`
        to clean up challenges.

        :param user: the requesting user
        :type user: User object
        :param passw: the password (pin+otp)
        :type passw: string
        :param options:  additional arguments from the request, which could be token specific
        :type options: dict
        :param challenges: A sorted list of valid challenges for this token.
        :type challenges: list
        :return: tuple of (otpcounter and the list of matching challenges)
        """
        otp_counter = -1
        transid = None
        matching = None
        matching_challenges = []

        # fetch the transactionid
        if 'transactionid' in options:
            transid = options.get('transactionid', None)

        # check if the transactionid is in the list of challenges
        if transid is not None:
            for challenge in challenges:
                if Challenges.is_same_transaction(challenge, transid):
                    matching = challenge
                    break
            if matching is not None:
                # Split pin from otp and check the resulting pin and otpval
                (pin, otpval) = self.splitPinPass(passw)
                if not check_pin(self, pin, user=user, options=options):
                    otpval = passw
                # The U2F checkOtp functions needs to know the saved challenge
                # to compare the received challenge value to the saved one,
                # thus we add the transactionid to the options
                options['transactionid'] = transid
                options['challenges'] = challenges
                otp_counter = check_otp(self, otpval, options=options)
                if otp_counter >= 0:
                    matching_challenges.append(matching)

        return (otp_counter, matching_challenges)
Example #9
0
    def check_by_transactionid(self, transid, passw, options=None):
        """
        check the passw against the open transaction

        :param transid: the transaction id
        :param passw: the pass parameter
        :param options: the additional optional parameters

        :return: tuple of boolean and detail dict
        """

        reply = {}

        serials = []
        challenges = Challenges.lookup_challenges(transid=transid)

        for challenge in challenges:
            serials.append(challenge.tokenserial)

        if not serials:
            reply['value'] = False
            reply['failure'] = ('No challenge for transaction %r found' %
                                transid)

            return False, reply

        reply['failcount'] = 0
        reply['value'] = False
        reply['token_type'] = ''

        for serial in serials:

            tokens = getTokens4UserOrSerial(serial=serial)
            if not tokens:
                raise Exception('tokenmismatch for token serial: %s' %
                                (unicode(serial)))

            # there could be only one
            token = tokens[0]
            owner = linotp.lib.token.get_token_owner(token)

            (ok, opt) = self.checkTokenList(tokens,
                                            passw,
                                            user=owner,
                                            options=options)
            if opt:
                reply.update(opt)

            reply['token_type'] = token.getType()
            reply['failcount'] = token.getFailCount()
            reply['value'] = ok

            if ok:
                break

        return ok, reply
Example #10
0
    def finish_invalid_tokens(self):
        """
        """
        invalid_tokens = self.invalid_tokens
        user = self.user

        for tok in invalid_tokens:
            tok.statusValidationFail()
            Challenges.finish_challenges(tok, success=False)

        import linotp.lib.policy
        pin_policies = linotp.lib.policy.get_pin_policies(user) or []

        if 1 in pin_policies:
            action_detail = "wrong user password -1"
        else:
            action_detail = "wrong otp pin -1"

        return (False, None, action_detail)
Example #11
0
    def finish_invalid_tokens(self):
        """
        """
        invalid_tokens = self.invalid_tokens
        user = self.user

        for tok in invalid_tokens:
            tok.statusValidationFail()
            Challenges.finish_challenges(tok, success=False)

        import linotp.lib.policy
        pin_policies = linotp.lib.policy.get_pin_policies(user) or []

        if 1 in pin_policies:
            action_detail = "wrong user password -1"
        else:
            action_detail = "wrong otp pin -1"

        return (False, None, action_detail)
Example #12
0
    def check_by_transactionid(self, transid, passw, options=None):
        """
        check the passw against the open transaction

        :param transid: the transaction id
        :param passw: the pass parameter
        :param options: the additional optional parameters

        :return: tuple of boolean and detail dict
        """

        reply = {}

        serials = []
        challenges = Challenges.lookup_challenges(transid=transid)

        for challenge in challenges:
            serials.append(challenge.tokenserial)

        if not serials:
            reply['value'] = False
            reply['failure'] = ('No challenge for transaction %r found'
                                % transid)

            return False, reply

        reply['failcount'] = 0
        reply['value'] = False
        reply['token_type'] = ''

        for serial in serials:

            tokens = getTokens4UserOrSerial(serial=serial)
            if not tokens:
                raise Exception('tokenmismatch for token serial: %s'
                                % (unicode(serial)))

            # there could be only one
            token = tokens[0]
            owner = linotp.lib.token.get_token_owner(token)

            (ok, opt) = self.checkTokenList(tokens, passw, user=owner,
                                            options=options)
            if opt:
                reply.update(opt)

            reply['token_type'] = token.getType()
            reply['failcount'] = token.getFailCount()
            reply['value'] = ok

            if ok:
                break

        return ok, reply
Example #13
0
    def test_transactionid_length(self):

        with patch('linotp.lib.challenges.context') as mock_context:
            mock_context.get.return_value = {}
            transid_length = Challenges.get_tranactionid_length()
            self.assertAlmostEqual(
                transid_length, Challenges.DefaultTransactionIdLength)

            with self.assertRaises(Exception) as wrong_range:
                too_short_length = 7

                wrong_range_message = \
                    "TransactionIdLength must be between 12 and 17, " \
                    "was %d" % too_short_length
                mock_context.get.return_value = {
                    'TransactionIdLength': too_short_length
                }
                Challenges.get_tranactionid_length()

            self.assertEqual(wrong_range.exception.message, wrong_range_message)
Example #14
0
    def test_transactionid_length(self):

        with patch('linotp.lib.challenges.context') as mock_context:
            mock_context.get.return_value = {}
            transid_length = Challenges.get_tranactionid_length()
            self.assertAlmostEqual(
                transid_length, Challenges.DefaultTransactionIdLength)

            with self.assertRaises(Exception) as wrong_range:
                too_short_length = 7

                wrong_range_message = \
                    "TransactionIdLength must be between 12 and 17, " \
                    "was %d" % too_short_length
                mock_context.get.return_value = {
                    'TransactionIdLength': too_short_length
                }
                Challenges.get_tranactionid_length()

            self.assertEqual(wrong_range.exception.message, wrong_range_message)
Example #15
0
    def test_transactionid_length(self):

        with patch('linotp.lib.challenges.context') as mock_context:
            mock_context.get.return_value = {}
            transid_length = Challenges.get_tranactionid_length()
            assert round(
                abs(transid_length - Challenges.DefaultTransactionIdLength),
                7) == 0

            too_short_length = 7

            wrong_range_message = \
                "TransactionIdLength must be between 12 and 17, " \
                "was %d" % too_short_length
            mock_context.get.return_value = {
                'TransactionIdLength': too_short_length
            }
            with pytest.raises(Exception) as wrong_range:
                Challenges.get_tranactionid_length()

            assert str(wrong_range.value) == wrong_range_message
Example #16
0
    def checkResponse4Challenge(self, user, passw, options=None, challenges=None):
        """
        This method verifies if the given ``passw`` matches any existing ``challenge``
        of the token.

        It then returns the new otp_counter of the token and the
        list of the matching challenges.

        In case of success the otp_counter needs to be > 0.
        The matching_challenges is passed to the method
        :py:meth:`~linotp.tokens.base.TokenClass.challenge_janitor`
        to clean up challenges.

        :param user: the requesting user
        :type user: User object
        :param passw: the password (pin+otp)
        :type passw: string
        :param options:  additional arguments from the request, which could be token specific
        :type options: dict
        :param challenges: A sorted list of valid challenges for this token.
        :type challenges: list
        :return: tuple of (otpcounter and the list of matching challenges)
        """
        otp_counter = -1
        transid = None
        matching = None
        matching_challenges = []

        # fetch the transactionid
        if 'transactionid' in options:
            transid = options.get('transactionid', None)

        # check if the transactionid is in the list of challenges
        if transid is not None:
            for challenge in challenges:
                if Challenges.is_same_transaction(challenge, transid):
                    matching = challenge
                    break
            if matching is not None:
                # Split pin from otp and check the resulting pin and otpval
                (pin, otpval) = self.splitPinPass(passw)
                if not check_pin(self, pin, user=user, options=options):
                    otpval = passw
                # The U2F checkOtp functions needs to know the saved challenge
                # to compare the received challenge value to the saved one,
                # thus we add the transactionid to the options
                options['transactionid'] = transid
                options['challenges'] = challenges
                otp_counter = check_otp(self, otpval, options=options)
                if otp_counter >= 0:
                    matching_challenges.append(matching)

        return (otp_counter, matching_challenges)
Example #17
0
    def checkResponse4Challenge(self,
                                user,
                                passw,
                                options=None,
                                challenges=None):
        '''
        This method verifies if the given ``passw`` matches any existing
        ``challenge`` of the token.

        It then returns the new otp_counter of the token and the
        list of the matching challenges.

        In case of success the otp_counter needs to be > 0.
        The matching_challenges is passed to the method
        :py:meth:`~linotp.tokens.base.TokenClass.challenge_janitor`
        to clean up challenges.

        :param user: the requesting user
        :type user: User object
        :param passw: the password (pin+otp)
        :type passw: string
        :param options:  additional arguments from the request, which could
                         be token specific
        :type options: dict
        :param challenges: A sorted list of valid challenges for this token.
        :type challenges: list
        :return: tuple of (otpcounter and the list of matching challenges)

        '''
        otp_counter = -1
        transid = None
        matching = None
        matching_challenges = []

        if 'transactionid' in options or 'state' in options:
            # fetch the transactionid
            transid = options.get('transactionid', None)
            if transid is None:
                transid = options.get('state', None)

        # check if the transactionid is in the list of challenges
        if transid is not None:
            for challenge in challenges:
                if Challenges.is_same_transaction(challenge, transid):
                    matching = challenge
                    break
            if matching is not None:
                otp_counter = check_otp(self, passw, options=options)
                if otp_counter >= 0:
                    matching_challenges.append(matching)

        return (otp_counter, matching_challenges)
Example #18
0
    def checkSerialPass(self, serial, passw, options=None, user=None):
        """
        This function checks the otp for a given serial

        :attention: the parameter user must be set, as the pin policy==1 will
                    verify the user pin
        """

        token_type = options.get("token_type", None)

        tokenList = getTokens4UserOrSerial(None,
                                           serial,
                                           token_type=token_type,
                                           read_for_update=True)

        if passw is None:
            # other than zero or one token should not happen, as serial is
            # unique
            if len(tokenList) == 1:
                theToken = tokenList[0]
                tok = theToken.token
                realms = tok.getRealmNames()
                if realms is None or len(realms) == 0:
                    realm = getDefaultRealm()
                elif len(realms) > 0:
                    realm = realms[0]
                userInfo = getUserInfo(
                    tok.LinOtpUserid,
                    tok.LinOtpIdResolver,
                    tok.LinOtpIdResClass,
                )
                user = User(login=userInfo.get("username"), realm=realm)
                user.info = userInfo

                if theToken.is_challenge_request(passw, user, options=options):
                    (res, opt) = Challenges.create_challenge(theToken, options)
                    res = False
                else:
                    raise ParameterError("Missing parameter: pass", id=905)

            else:
                raise Exception("No token found: "
                                "unable to create challenge for %s" % serial)

        else:
            (res, opt) = self.checkTokenList(tokenList,
                                             passw,
                                             user=user,
                                             options=options)

        return (res, opt)
Example #19
0
    def finish_invalid_tokens(self):
        """"""
        invalid_tokens = self.invalid_tokens
        user = self.user

        for tok in invalid_tokens:

            # count all token accesses
            if tok.count_auth_max > 0:
                tok.inc_count_auth()

            tok.statusValidationFail()

            Challenges.finish_challenges(tok, success=False)

        pin_policies = get_pin_policies(user) or []

        if 1 in pin_policies:
            action_detail = "wrong user password -1"
        else:
            action_detail = "wrong otp pin -1"

        return (False, None, action_detail)
Example #20
0
    def checkResponse4Challenge(self, user, passw, options=None,
                                challenges=None):
        '''
        This method verifies if the given ``passw`` matches any existing
        ``challenge`` of the token.

        It then returns the new otp_counter of the token and the
        list of the matching challenges.

        In case of success the otp_counter needs to be > 0.
        The matching_challenges is passed to the method
        :py:meth:`~linotp.tokens.base.TokenClass.challenge_janitor`
        to clean up challenges.

        :param user: the requesting user
        :type user: User object
        :param passw: the password (pin+otp)
        :type passw: string
        :param options:  additional arguments from the request, which could
                         be token specific
        :type options: dict
        :param challenges: A sorted list of valid challenges for this token.
        :type challenges: list
        :return: tuple of (otpcounter and the list of matching challenges)

        '''
        otp_counter = -1
        transid = None
        matching = None
        matching_challenges = []

        if 'transactionid' in options or 'state' in options:
            # fetch the transactionid
            transid = options.get('transactionid', None)
            if transid is None:
                transid = options.get('state', None)

        # check if the transactionid is in the list of challenges
        if transid is not None:
            for challenge in challenges:
                if Challenges.is_same_transaction(challenge, transid):
                    matching = challenge
                    break
            if matching is not None:
                otp_counter = check_otp(self, passw, options=options)
                if otp_counter >= 0:
                    matching_challenges.append(matching)

        return (otp_counter, matching_challenges)
Example #21
0
    def finish_invalid_tokens(self):
        """
        """
        invalid_tokens = self.invalid_tokens
        user = self.user

        for tok in invalid_tokens:

            # count all token accesses
            if tok.count_auth_max > 0:
                tok.inc_count_auth()

            tok.statusValidationFail()

            Challenges.finish_challenges(tok, success=False)

        pin_policies = get_pin_policies(user) or []

        if 1 in pin_policies:
            action_detail = "wrong user password -1"
        else:
            action_detail = "wrong otp pin -1"

        return (False, None, action_detail)
Example #22
0
    def checkSerialPass(self, serial, passw, options=None, user=None):
        """
        This function checks the otp for a given serial

        :attention: the parameter user must be set, as the pin policy==1 will
                    verify the user pin
        """

        log.debug('checking for serial %r' % serial)
        tokenList = linotp.lib.token.getTokens4UserOrSerial(
            None, serial)

        if passw is None:
            # other than zero or one token should not happen, as serial is
            # unique
            if len(tokenList) == 1:
                theToken = tokenList[0]
                tok = theToken.token
                realms = tok.getRealmNames()
                if realms is None or len(realms) == 0:
                    realm = getDefaultRealm()
                elif len(realms) > 0:
                    realm = realms[0]
                userInfo = getUserInfo(tok.LinOtpUserid, tok.LinOtpIdResolver,
                                       tok.LinOtpIdResClass)
                user = User(login=userInfo.get('username'), realm=realm)
                user.info = userInfo

                if theToken.is_challenge_request(passw, user, options=options):
                    (res, opt) = Challenges.create_challenge(
                        theToken, options)
                    res = False
                else:
                    raise ParameterError('Missing parameter: pass', id=905)

            else:
                raise Exception('No token found: '
                                'unable to create challenge for %s' % serial)

        else:
            log.debug('checking len(pass)=%r for serial %r' %
                      (len(passw), serial))

            (res, opt) = self.checkTokenList(
                tokenList, passw, user=user, options=options)

        return (res, opt)
Example #23
0
    def checkSerialPass(self, serial, passw, options=None, user=None):
        """
        This function checks the otp for a given serial

        :attention: the parameter user must be set, as the pin policy==1 will
                    verify the user pin
        """

        log.debug('checking for serial %r' % serial)
        tokenList = linotp.lib.token.getTokens4UserOrSerial(None, serial)

        if passw is None:
            # other than zero or one token should not happen, as serial is
            # unique
            if len(tokenList) == 1:
                theToken = tokenList[0]
                tok = theToken.token
                realms = tok.getRealmNames()
                if realms is None or len(realms) == 0:
                    realm = getDefaultRealm()
                elif len(realms) > 0:
                    realm = realms[0]
                userInfo = getUserInfo(tok.LinOtpUserid, tok.LinOtpIdResolver,
                                       tok.LinOtpIdResClass)
                user = User(login=userInfo.get('username'), realm=realm)
                user.info = userInfo

                if theToken.is_challenge_request(passw, user, options=options):
                    (res, opt) = Challenges.create_challenge(theToken, options)
                    res = False
                else:
                    raise ParameterError('Missing parameter: pass', id=905)

            else:
                raise Exception('No token found: '
                                'unable to create challenge for %s' % serial)

        else:
            log.debug('checking len(pass)=%r for serial %r' %
                      (len(passw), serial))

            (res, opt) = self.checkTokenList(tokenList,
                                             passw,
                                             user=user,
                                             options=options)

        return (res, opt)
Example #24
0
    def statusValidationFail(self):
        '''
        statusValidationFail - callback to enable a status change,

        will be called if the token verification has failed

        :return - nothing

        '''
        log.debug('[statusValidationFail]')
        challenge = None

        if self.transId == 0 :
            return
        try:

            challenges = Challenges.lookup_challenges(self.context, self.getSerial(),
                                                      transid=self.transId)
            if len(challenges) == 1:
                challenge = challenges[0]
                challenge.setTanStatus(received=True, valid=False)


            ##  still in rollout state??
            rolloutState = self.getFromTokenInfo('rollout', '0')

            if rolloutState == '1':
                log.info('rollout state 1 for token %r not completed' % (self.getSerial()))

            elif rolloutState == '2':
                if challenge.received_count >= int(getFromConfig("OcraMaxChallengeRequests", '3')):
                    ##  after 3 fails in rollout state 2 - reset to rescan
                    self.addToTokenInfo('rollout', '1')
                    log.info('rollout for token %r reset to phase 1:' % (self.getSerial()))

                log.info('rollout for token %r not completed' % (self.getSerial()))

        except Exception as ex:
            log.exception('[Ocra2TokenClass:statusValidationFail] Error during validation finalisation for token %r :%r' % (self.getSerial(), ex))
            raise Exception(ex)

        finally:
            if challenge is not None:
                challenge.save()

        log.debug('[statusValidationFail]')
        return
Example #25
0
    def check_challenge_response(self, challenges, user, passw, options=None):
        """
        This function checks, if the given response (passw) matches
        any of the open challenges

        to prevent the token author to deal with the database layer, the
        token.checkResponse4Challenge will recieve only the dictionary of the
        challenge data

        :param challenges: the list of database challenges
        :param user: the requesting use
        :param passw: the to password of the request, which must be pin+otp
        :param options: the addtional request parameters
        :return: tuple of otpcount (as result of an internal token.checkOtp)
                 and additional optional reply
        """
        # challenge reply will stay None as we are in the challenge response
        # mode
        reply = None
        if options is None:
            options = {}

        otp = passw
        self.transId = options.get('transactionid', options.get('state', None))

        # only check those challenges, which currently have not been verified
        check_challenges = []
        for ch in challenges:
            if Challenges.verify_checksum(ch) and ch.is_open():
                check_challenges.append(ch)

        (otpcount, matching_challenges) = self.checkResponse4Challenge(
            user, otp, options=options, challenges=check_challenges)

        if otpcount >= 0:
            self.matching_challenges = matching_challenges
            self.valid_token.append(self)
            if len(self.invalid_token) > 0:
                del self.invalid_token[0]
        else:
            self.invalid_token.append(self)

        return (otpcount, reply)
Example #26
0
    def check_challenge_response(self, challenges, user, passw, options=None):
        """
        This function checks, if the given response (passw) matches
        any of the open challenges

        to prevent the token author to deal with the database layer, the
        token.checkResponse4Challenge will recieve only the dictionary of the
        challenge data

        :param challenges: the list of database challenges
        :param user: the requesting use
        :param passw: the to password of the request, which must be pin+otp
        :param options: the addtional request parameters
        :return: tuple of otpcount (as result of an internal token.checkOtp)
                 and additional optional reply
        """
        # challenge reply will stay None as we are in the challenge response
        # mode
        reply = None
        if options is None:
            options = {}

        otp = passw
        self.transId = options.get('transactionid', options.get('state', None))

        # only check those challenges, which currently have not been verified
        check_challenges = []
        for ch in challenges:
            if Challenges.verify_checksum(ch) and ch.is_open():
                check_challenges.append(ch)

        (otpcount, matching_challenges) = self.checkResponse4Challenge(
            user, otp, options=options, challenges=check_challenges)

        if otpcount >= 0:
            self.matching_challenges = matching_challenges
            self.valid_token.append(self)
            if len(self.invalid_token) > 0:
                del self.invalid_token[0]
        else:
            self.invalid_token.append(self)

        return (otpcount, reply)
Example #27
0
    def checkResponse4Challenge(self,
                                user,
                                passw,
                                options=None,
                                challenges=None):
        '''
        verify the response of a previous challenge

        :param user:     the requesting user
        :param passw:    the to be checked pass (pin+otp)
        :param options:  options an additional argument, which could be token
                          specific
        :param challenges: the list of challenges, where each challenge is
                            described as dict
        :return: tuple of (otpcounter and the list of matching challenges)

        '''
        otp_counter = -1
        transid = None
        matching = None
        matching_challenges = []

        if 'transactionid' in options or 'state' in options:
            ## fetch the transactionid
            transid = options.get('transactionid', options.get('state', None))

        if not transid and self.authenticated is not None:
            pin_match, otp_counter, reply = self.authenticated
            return otp_counter, matching_challenges

        # check if the transactionid is in the list of challenges
        if transid is not None:
            for challenge in challenges:
                if Challenges.is_same_transaction(challenge, transid):
                    matching = challenge
                    break
            if matching is not None:
                otp_counter = check_otp(self, passw, options=options)
                if otp_counter >= 0:
                    matching_challenges.append(matching)

        return (otp_counter, matching_challenges)
Example #28
0
    def statusValidationSuccess(self):
        '''
        statusValidationSuccess - callback to enable a status change,

        remark: will be called if the token has been succesfull verified

        :return: - nothing

        '''
        log.debug('[statusValidationSuccess]')

        if self.transId == 0 :
            return

        challenges = Challenges.lookup_challenges(self.context, self.getSerial(),
                                                  transid=self.transId)
        if len(challenges) == 1:
            challenge = challenges[0]
            challenge.setTanStatus(True, True)
            challenge.save()

        ##  still in rollout state??
        rolloutState = self.getFromTokenInfo('rollout', '0')

        if rolloutState == '2':
            t_info = self.getTokenInfo()
            if t_info.has_key('rollout'):
                del t_info['rollout']
            if t_info.has_key('sharedSecret'):
                del t_info['sharedSecret']
            if t_info.has_key('nonce'):
                del t_info['nonce']
            self.setTokenInfo(t_info)

            log.info('rollout for token %r completed' % (self.getSerial()))

        elif rolloutState == '1':
            raise Exception('unable to complete the rollout ')

        log.debug('[statusValidationSuccess]:')
        return
Example #29
0
    def getStatus(self, transactionId):
        '''
        getStatus - assembles the status of a transaction / challenge in a dict

        {   "serial": SERIENNUMMER1,
            "transactionid": TRANSACTIONID1,
            "received_tan": true,
            "valid_tan": true,
            "failcount": 0
        }

        :param transactionId:    the transaction / challenge id
        :type transactionId:    string

        :return:    status dict
        :rtype:       dict
        '''

        log.debug('[getStatus] %r' % (transactionId))

        statusDict = {}
        challenge = Challenges.lookup_challenges(self.context, self.getSerial(),
                                                 transid=transactionId)
        if challenge is not None:
            statusDict['serial'] = challenge.tokenserial
            statusDict['transactionid'] = challenge.transid
            statusDict['received_tan'] = challenge.received_tan
            statusDict['valid_tan'] = challenge.valid_tan
            statusDict['failcount'] = self.getFailCount()
            statusDict['id'] = challenge.id
            statusDict['timestamp'] = unicode(challenge.timestamp)
            statusDict['active'] = unicode(self.isActive())


        log.debug('[getStatus]: %r' % (statusDict))
        return statusDict
Example #30
0
    def challenge(self, data, session='', typ='raw', challenge=None):
        '''
        the challenge method is for creating an transaction / challenge object

        remark: the transaction has a maximum lifetime and a reference to
                the OcraSuite token (serial)

        :param data:     data, which is the base for the challenge or None
        :type data:     string or None
        :param session:  session support for ocratokens
        :type session:  string
        :type typ:      define, which kind of challenge base should be used
                         could be raw - take the data input as is
                               (extract chars accordind challenge definition Q)
                         or random    - will generate a random input
                         or hased     - will take the hash of the input data

        :return:    challenge response containing the transcation id and the
                    challenge for the ocrasuite
        :rtype :    tuple of (transId(string), challenge(string))


        '''
        log.debug('[challenge] %r: %r: %r' % (data, session, challenge))

        secretHOtp = self.token.getHOtpKey()
        ocraSuite = OcraSuite(self.getOcraSuiteSuite(), secretHOtp)

        if data is None or len(data) == 0:
            typ = 'random'

        if challenge is None:
            if typ == 'raw':
                challenge = ocraSuite.data2rawChallenge(data)
            elif typ == 'random':
                challenge = ocraSuite.data2randomChallenge(data)
            elif typ == 'hash':
                challenge = ocraSuite.data2hashChallenge(data)

        log.debug('[Ocra2TokenClass] challenge: %r ' % (challenge))

        counter = self.getOtpCount()

        ## set the pin onyl in the compliant hashed mode
        pin = ''
        if ocraSuite.P is not None:
            pinObj = self.token.getUserPin()
            pin = pinObj.getKey()

        try:
            param = {}
            param['C'] = counter
            param['Q'] = challenge
            param['P'] = pin
            param['S'] = session
            if ocraSuite.T is not None:
                now = datetime.datetime.now()
                stime = now.strftime("%s")
                itime = int(stime)
                param['T'] = itime

            ''' verify that the data is compliant with the OcraSuitesuite
                and the client is able to calc the otp
            '''
            c_data = ocraSuite.combineData(**param)
            ocraSuite.compute(c_data)

        except Exception as ex:
            raise Exception('[Ocra2TokenClass] Failed to create ocrasuite '
                                                        'challenge: %r' % (ex))

        ##  create a non exisiting challenge
        try:

            (res, opt) = Challenges.create_challenge(self, self.context, options={'messgae': data})

            transid = opt.get('transactionid')
            challenge = opt.get('challenge')

        except Exception as ex:
            ##  this might happen if we have a db problem or
            ##   the uniqnes constrain does not fit
            log.exception("[Ocra2TokenClass] %r" % ex)
            raise Exception('[Ocra2TokenClass] Failed to create '
                                                'challenge object: %s' % (ex))

        realm = None
        realms = self.token.getRealms()
        if len(realms) > 0:
            realm = realms[0]

        url = ''
        if realm is not None:
            url = get_qrtan_url(realm.name, context=self.context)

        log.debug('[challenge]: %r: %r: %r' % (transid, challenge, url))
        return (transid, challenge, True, url)
Example #31
0
    def checkResponse4Challenge(self, user, passw, options=None,
                                challenges=None):
        """
        verify the response of a previous challenge

        There are two possible cases:

        1) The 'transaction_id' (also know as 'state', which has the same
           value) is available in options
        2) No 'transaction_id'

        In the first case we can safely assume that the passw only contains
        the OTP (no pin). In the second case passw will contain both and we
        split to get the OTP.

        :param user:     the requesting user
        :param passw:    the to be checked pass (pin+otp)
        :param options:  options an additional argument, which could be token
                          specific
        :param challenges: the list of challenges, where each challenge is
                            described as dict
        :return: tuple of (otpcounter and the list of matching challenges)

        """
        transaction_id = None
        otp_counter = -1
        matching_challenges = []

        if challenges is None or len(challenges) == 0:
            # There are no challenges for this token
            return -1, []

        if options and ('transactionid' in options or 'state' in options):
            # fetch the transactionid
            transaction_id = options.get('transactionid', None)
            if transaction_id is None:
                transaction_id = options.get('state', None)

        if transaction_id:
            otp = passw
            # if the transaction_id is set we can assume that we have only
            # received a single challenge with that transaction_id thanks to
            # linotp.lib.validate.ValidateToken.get_challenges()
            assert(len(challenges) == 1)
            assert(Challenges.is_same_transaction(challenges[0], transaction_id))
        else:
            # If no transaction_id is set the request came through the WebUI
            # and we have to check all challenges
            split_status, pin, otp = split_pin_otp(self, passw, user, options)
            if split_status < 0:
                raise Exception("Could not split passw")
            if not check_pin(self, pin, user, options):
                return -1, []

        window = self.getOtpCountWindow()

        for challenge in challenges:
            challenge_data = challenge.getData()
            stored_counter = challenge_data.get("counter_value")
            temp_otp_counter = self.checkOtp(otp, int(stored_counter),
                                             window, options)
            if temp_otp_counter > 0:
                otp_counter = temp_otp_counter
                matching_challenges = [challenge]
                break

        # The matching_challenges list will either contain a single challenge
        # or will be empty. Returning multiple challenges is not useful in this
        # case because all older challenges arecleaned up anyway.
        return otp_counter, matching_challenges
Example #32
0
    def checkOtp(self, passw , counter, window, options=None):
        '''
        checkOtp - standard callback of linotp to verify the token

        :param passw:      the passw / otp, which has to be checked
        :type passw:       string
        :param counter:    the start counter
        :type counter:     int
        :param  window:    the window, in which the token is valid
        :type  window:     int
        :param options:    options contains the transaction id,
                            eg. if check_t checks one transaction
                            this will support assynchreonous otp checks
                            (when check_t is used)
        :type options:     dict

        :return:           verification counter or -1
        :rtype:            int (-1)

        '''
        log.debug('[checkOtp] %r: %r: %r' % (passw, counter, window))
        ret = -1

        challenges = []
        serial = self.getSerial()

        if options is None:
            options = {}

        maxRequests = int(getFromConfig("Ocra2MaxChallengeRequests", '3'))

        if 'transactionid' in options:
            transid = options.get('transactionid', None)
            challs = Challenges.lookup_challenges(self.context, serial=serial,
                                                  transid=transid)
            for chall in challs:
                (rec_tan, rec_valid) = chall.getTanStatus()
                if rec_tan == False:
                    challenges.append(chall)
                elif rec_valid == False:
                    ## add all touched but failed challenges
                    if chall.getTanCount() <= maxRequests:
                        challenges.append(chall)

        if 'challenge' in options:
            ## direct challenge - there might be addtionalget info like
            ## session data in the options
            challenges.append(options)

        if len(challenges) == 0:
            challs = Challenges.lookup_challenges(self.context, serial=serial)
            for chall in challs:
                (rec_tan, rec_valid) = chall.getTanStatus()
                if rec_tan == False:
                    ## add all untouched challenges
                    challenges.append(chall)
                elif rec_valid == False:
                    ## add all touched but failed challenges
                    if chall.getTanCount() <= maxRequests:
                        challenges.append(chall)

        if len(challenges) == 0:
            err = 'No open transaction found for token %s' % serial
            log.error(err)  ##TODO should log and fail!!
            raise Exception(err)

        ## prepare the challenge check - do the ocra setup
        secretHOtp = self.token.getHOtpKey()
        ocraSuite = OcraSuite(self.getOcraSuiteSuite(), secretHOtp)

        ## set the ocra token pin
        ocraPin = ''
        if ocraSuite.P is not None:
            ocraPinObj = self.token.getUserPin()
            ocraPin = ocraPinObj.getKey()

            if ocraPin is None or len(ocraPin) == 0:
                ocraPin = ''

        timeShift = 0
        if  ocraSuite.T is not None:
            defTimeWindow = int(getFromConfig("ocra.timeWindow", 180))
            window = int(self.getFromTokenInfo('timeWindow', defTimeWindow)) / ocraSuite.T
            defTimeShift = int(getFromConfig("ocra.timeShift", 0))
            timeShift = int(self.getFromTokenInfo("timeShift", defTimeShift))

        default_retry_window = int(getFromConfig("ocra2.max_check_challenge_retry", 0))
        retry_window = int(self.getFromTokenInfo("max_check_challenge_retry", default_retry_window))

        ## now check the otp for each challenge

        for ch in challenges:
            challenge = {}

            ##  preserve transaction context, so we could use this in the status callback
            self.transId = ch.get('transid', None)
            challenge['transid'] = self.transId
            challenge['session'] = ch.get('session', None)

            ## we saved the 'real' challenge in the data
            data = ch.get('data', None)
            if data is not None:
                challenge['challenge'] = data.get('challenge')
            elif 'challenge' in ch:
                ## handle explicit challenge requests
                challenge['challenge'] = ch.get('challenge')

            if challenge.get('challenge') is None:
                raise Exception('could not checkOtp due to missing challenge'
                                ' in request: %r' % ch)

            ret = ocraSuite.checkOtp(passw, counter, window, challenge, pin=ocraPin , options=options, timeshift=timeShift)
            log.debug('[checkOtp]: %r' % (ret))

            ## due to the assynchronous challenge verification of the checkOtp
            ## it might happen, that the found counter is lower than the given
            ## one. Thus we fix this here to deny assynchronous verification

            # we do not support retry checks anymore:
            # which means, that ret might be smaller than the actual counter
            if ocraSuite.T is None:
                if ret + retry_window < counter:
                    ret = -1

            if ret != -1:
                break

        if -1 == ret:
            ##  autosync: test if two consecutive challenges + it's counter match
            ret = self.autosync(ocraSuite, passw, challenge)


        return ret
Example #33
0
    def checkOtp(self, passwd, counter, window, options=None):

        valid_states = ['pairing_challenge_sent',
                        'pairing_complete']

        self.ensure_state_is_in(valid_states)

        # ----------------------------------------------------------------------

        filtered_challenges = []
        serial = self.getSerial()

        if options is None:
            options = {}

        max_fail = int(getFromConfig('QRMaxChallenges', '3'))

        # ----------------------------------------------------------------------

        # TODO: from which point is checkOtp called, when there
        # is no challenge response in the request?

        if 'transactionid' in options:

            # ------------------------------------------------------------------

            # fetch all challenges that match the transaction id or serial

            transaction_id = options.get('transaction_id')

            challenges = Challenges.lookup_challenges(serial, transaction_id)

            # ------------------------------------------------------------------

            # filter into filtered_challenges

            for challenge in challenges:

                (received_tan, tan_is_valid) = challenge.getTanStatus()
                fail_counter = challenge.getTanCount()

                # if we iterate over matching challenges (that is: challenges
                # with the correct transaction id) we either find a fresh
                # challenge, that didn't receive a TAN at all (first case)
                # or a challenge, that already received a number of wrong
                # TANs but still has tries left (second case).

                if not received_tan:
                    filtered_challenges.append(challenge)
                elif not tan_is_valid and fail_counter <= max_fail:
                    filtered_challenges.append(challenge)

            # ------------------------------------------------------------------

        if not filtered_challenges:
            return -1

        for challenge in filtered_challenges:

            data = challenge.getData()
            correct_passwd = data['user_sig']

            # compare values with python's native constant
            # time comparison

            if compare_digest(correct_passwd, passwd):

                return 1

            else:

                # maybe we got a tan instead of a signature

                correct_passwd_as_bytes = decode_base64_urlsafe(correct_passwd)
                tan_length = 8  # TODO fetch from policy
                correct_tan = extract_tan(correct_passwd_as_bytes, tan_length)

                # TODO PYLONS-HACK pylons silently converts integers in
                # incoming json to unicode. since extract_tan returns
                # an integer, we have to convert it here
                correct_tan = unicode(correct_tan)

                if compare_digest(correct_tan, passwd):
                    return 1

        return -1  # TODO: ??? semantics of this ret val?
Example #34
0
    def check_status(self,
                     transid=None,
                     user=None,
                     serial=None,
                     password=None,
                     use_offline=False):
        """
        check for open transactions - for polling support

        :param transid: the transaction id where we request the status from
        :param user: the token owner user
        :param serial: or the serial we are searching for
        :param password: the pin/password for authorization the request
        :param use_offline: on success the offline info is returned

        :return: tuple of success and detail dict
        """

        expired, challenges = Challenges.get_challenges(None, transid=transid)

        # remove all expired challenges
        if expired:
            Challenges.delete_challenges(None, expired)

        if not challenges:
            return False, None

        # there is only one challenge per transaction id
        # if not multiple challenges, where transaction id is the parent one
        reply = {}

        pin_policies = get_pin_policies(user)
        if 1 in pin_policies:
            pin_match = check_pin(None, password, user=user, options=None)
            if not pin_match:
                return False, None

        involved_tokens = []

        transactions = {}
        for ch in challenges:

            # only look for challenges that are not compromised
            if not Challenges.verify_checksum(ch):
                continue

            # is the requester authorized
            serial = ch.getTokenSerial()
            tokens = getTokens4UserOrSerial(serial=serial)
            if not tokens:
                continue
            involved_tokens.extend(tokens)

            # as one challenge belongs exactly to only one token,
            # we take this one as the token
            token = tokens[0]

            if 1 not in pin_policies:
                pin_match = check_pin(token, password, user=user, options=None)
                if not pin_match:
                    ret = False
                    continue

            ret = True

            trans_dict = {}

            trans_dict['received_count'] = ch.received_count
            trans_dict['received_tan'] = ch.received_tan
            trans_dict['valid_tan'] = ch.valid_tan
            trans_dict['message'] = ch.challenge
            trans_dict['status'] = ch.getStatus()

            token_dict = {'serial': serial, 'type': token.type}

            # 1. check if token supports offline at all
            supports_offline_at_all = token.supports_offline_mode

            # 2. check if policy allows to use offline authentication
            if user is not None and user.login and user.realm:
                realms = [user.realm]
            else:
                realms = token.getRealms()

            offline_is_allowed = supports_offline(realms, token)

            if not ch.is_open() and ch.valid_tan and \
               supports_offline_at_all and \
               offline_is_allowed and \
               use_offline:
                token_dict['offline_info'] = token.getOfflineInfo()

            trans_dict['token'] = token_dict
            transactions[ch.transid] = trans_dict

        if transactions:
            reply['transactions'] = transactions

        return ret, reply
Example #35
0
    def checkOtp(self, passwd, counter, window, options=None):

        """
        checks if the supplied challenge response is correct.

        :param passwd: The challenge response

        :param options: A dictionary of parameters passed by the upper
            layer (used for transaction_id in this context)

        :param counter: legacy API (unused)

        :param window: legacy API (unused)

        :raises TokenStateError: If token state is not 'active' or
            'pairing_challenge_sent'

        :returns: -1 for failure, 1 for success
        """

        valid_states = ['pairing_challenge_sent',
                        'active']

        self.ensure_state_is_in(valid_states)

        # ------------------------------------------------------------------ --

        # new pushtoken protocoll supports the keyword based accept or deny.
        # the old 'passwd' argument is not supported anymore

        try:

            signature_accept = passwd.get('accept', None)
            signature_reject = passwd.get('reject', None)

        except AttributeError:  # will be raised with a get() on a str object

            raise Exception('Pushtoken version %r requires "accept" or'
                            ' "reject" as parameter' % CHALLENGE_URL_VERSION)

        if signature_accept is not None and signature_reject is not None:

            raise Exception('Pushtoken version %r requires "accept" or'
                            ' "reject" as parameter' % CHALLENGE_URL_VERSION)

        # ------------------------------------------------------------------ --

        filtered_challenges = []
        serial = self.getSerial()

        if options is None:
            options = {}

        max_fail = int(getFromConfig('PushMaxChallenges', '3'))

        # ------------------------------------------------------------------ --

        if 'transactionid' in options:

            # -------------------------------------------------------------- --

            # fetch all challenges that match the transaction id or serial

            transaction_id = options.get('transactionid')

            challenges = Challenges.lookup_challenges(serial=serial,
                transid=transaction_id, filter_open=True)

            # -------------------------------------------------------------- --

            # filter into filtered_challenges

            for challenge in challenges:

                (received_tan, tan_is_valid) = challenge.getTanStatus()
                fail_counter = challenge.getTanCount()

                # if we iterate over matching challenges (that is: challenges
                # with the correct transaction id) we either find a fresh
                # challenge, that didn't receive a TAN at all (first case)
                # or a challenge, that already received a number of wrong
                # TANs but still has tries left (second case).

                if not received_tan:
                    filtered_challenges.append(challenge)
                elif not tan_is_valid and fail_counter <= max_fail:
                    filtered_challenges.append(challenge)

        # ------------------------------------------------------------------ --

        if not filtered_challenges:
            return -1

        if len(filtered_challenges) > 1:
            log.error('multiple challenges for one transaction and for one'
                      ' token found!')
            return -1

        # for the serial and the transaction id there could always be only
        # at max one challenge matching. This is even true for sub transactions

        challenge = filtered_challenges[0]

        # client verifies the challenge by signing the challenge
        # plaintext. we retrieve the original plaintext (saved
        # in createChallenge) and check for a match

        data = challenge.getData()
        data_to_verify = b64decode(data['sig_base'])

        b64_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key')
        user_dsa_public_key = b64decode(b64_dsa_public_key)

        # -------------------------------------------------------------- --

        # handle the accept case

        if signature_accept is not None:

            accept_signature_as_bytes = decode_base64_urlsafe(
                                                    signature_accept)

            accept_data_to_verify_as_bytes = (
                            struct.pack('<b', CHALLENGE_URL_VERSION) +
                            b'ACCEPT\0' +
                            data_to_verify)

            try:
                verify_sig(accept_signature_as_bytes,
                           accept_data_to_verify_as_bytes,
                           user_dsa_public_key)

                challenge.add_session_info({'accept': True})

                return 1

            except ValueError:

                challenge.add_session_info({'accept': False})
                log.error("accept signature mismatch!")

                return -1

        # -------------------------------------------------------------- --

        # handle the reject case

        elif signature_reject is not None:

            reject_signature_as_bytes = decode_base64_urlsafe(
                                                        signature_reject)

            reject_data_to_verify_as_bytes = (
                            struct.pack('<b', CHALLENGE_URL_VERSION) +
                            b'DENY\0' +
                            data_to_verify)

            try:
                verify_sig(reject_signature_as_bytes,
                           reject_data_to_verify_as_bytes,
                           user_dsa_public_key)

                challenge.add_session_info({'reject': True})

                return 1

            except ValueError:

                challenge.add_session_info({'reject': False})
                log.error("reject signature mismatch!")

                return -1

        return -1
Example #36
0
    def finish_valid_tokens(self):
        """
        processing of the valid tokens
        """
        valid_tokens = self.valid_tokens
        validation_results = self.validation_results
        user = self.user

        if len(valid_tokens) == 1:
            token = valid_tokens[0]
            if user:
                action_detail = ("user %r@%r successfully authenticated." %
                                 (user.login, user.realm))
            else:
                action_detail = ("serial %r successfully authenticated." %
                                 token.getSerial())

            log.info(action_detail)

            # there could be a match in the window ahead,
            # so we need the last valid counter here
            (counter, _reply) = validation_results[token.getSerial()]
            token.setOtpCount(counter + 1)
            token.statusValidationSuccess()
            # finish as well related open challenges
            Challenges.finish_challenges(token, success=True)

            if token.getFromTokenInfo('count_auth_success_max', default=None):
                auth_count = token.get_count_auth_success()
                token.set_count_auth_success(auth_count + 1)

            detail = None
            auth_info = self.options.get('auth_info', 'False')
            if auth_info.lower() == "true":
                detail = token.getAuthDetail()

            # 1. check if token supports offline at all
            supports_offline_at_all = token.supports_offline_mode

            # 2. check if policy allows to use offline authentication
            if user is not None and user.login and user.realm:
                realms = [user.realm]
            else:
                realms = token.getRealms()

            offline_is_allowed = supports_offline(realms, token)

            # 3. check if parameter 'use_offline' is provided
            use_offline_param = self.options.get('use_offline', 'False')
            use_offline = use_offline_param.lower() == 'true'

            if supports_offline_at_all and \
               offline_is_allowed and \
               use_offline:

                offline_info = token.getOfflineInfo()
                if detail is None:
                    detail = {}

                offline = {'serial': token.getSerial(), 'type': token.type}
                offline['offline_info'] = offline_info

                detail.update({'offline': offline})

            return (True, detail, action_detail)

        else:
            # we have to set the matching counter to prevent replay one one
            # single token
            for token in valid_tokens:
                (res, _reply) = validation_results[token.getSerial()]
                token.setOtpCount(res)

            context['audit']['action_detail'] = "Multiple valid tokens found!"
            if user:
                log.error(
                    "[__checkTokenList] multiple token match error: "
                    "Several Tokens matching with the same OTP PIN "
                    "and OTP for user %r. Not sure how to auth", user.login)
            raise UserError("multiple token match error", id=-33)
Example #37
0
    def finish_valid_tokens(self):
        """
        processing of the valid tokens
        """
        valid_tokens = self.valid_tokens
        validation_results = self.validation_results
        user = self.user

        if len(valid_tokens) == 1:
            token = valid_tokens[0]
            if user:
                action_detail = "user %r@%r successfully authenticated." % (
                    user.login,
                    user.realm,
                )
            else:
                action_detail = ("serial %r successfully authenticated." %
                                 token.getSerial())

            log.info(action_detail)

            # there could be a match in the window ahead,
            # so we need the last valid counter here

            (counter, _reply) = validation_results[token.getSerial()]
            token.setOtpCount(counter + 1)
            token.statusValidationSuccess()

            # finish as well related open challenges
            Challenges.finish_challenges(token, success=True)

            if token.count_auth_success_max > 0:
                token.inc_count_auth_success()

            if token.count_auth_max > 0:
                token.inc_count_auth()

            detail = None
            auth_info = self.options.get("auth_info", "False")
            if auth_info.lower() == "true":
                detail = token.getAuthDetail()

            # 1. check if token supports offline at all
            supports_offline_at_all = token.supports_offline_mode

            # 2. check if policy allows to use offline authentication
            if user is not None and user.login and user.realm:
                realms = [user.realm]
            else:
                realms = token.getRealms()

            offline_is_allowed = supports_offline(realms, token)

            # 3. check if parameter 'use_offline' is provided
            use_offline_param = self.options.get("use_offline", "False")
            use_offline = use_offline_param.lower() == "true"

            if supports_offline_at_all and offline_is_allowed and use_offline:

                offline_info = token.getOfflineInfo()
                if detail is None:
                    detail = {}

                offline = {"serial": token.getSerial(), "type": token.type}
                offline["offline_info"] = offline_info

                detail.update({"offline": offline})

            janitor_to_remove_enrollment_token(valid_tokens=[token])

            return (True, detail, action_detail)

        else:
            # we have to set the matching counter to prevent replay one one
            # single token

            for token in valid_tokens:

                (res, _reply) = validation_results[token.getSerial()]

                token.setOtpCount(res)

                # in case of multiple matches the tokens were accessed
                # so we count them as well
                if token.count_auth_max > 0:
                    token.inc_count_auth()

            janitor_to_remove_enrollment_token(valid_tokens=valid_tokens)

            g.audit["action_detail"] = "Multiple valid tokens found!"
            if user:
                log.error(
                    "multiple token match error: "
                    "Several Tokens matching with the same OTP PIN "
                    "and OTP for user %r. Not sure how to auth",
                    user.login,
                )

            raise UserError("multiple token match error", id=-33)
Example #38
0
    def checkTokenList(self, tokenList, passw, user=User(), options=None):
        """
        identify a matching token and test, if the token is valid, locked ..
        This function is called by checkSerialPass and checkUserPass to

        :param tokenList: list of identified tokens
        :param passw: the provided passw (mostly pin+otp)
        :param user: the identified use - as class object
        :param options: additional parameters, which are passed to the token

        :return: tuple of boolean and optional response
        """
        reply = None

        #  add the user to the options, so that every token could see the user
        if not options:
            options = {}

        options["user"] = user

        # if there has been one token in challenge mode, we only handle
        # challenges

        # if we got a validation against a sub_challenge, we extend this to
        # be a validation to all challenges of the transaction id
        import copy

        check_options = copy.deepcopy(options)
        state = check_options.get("state",
                                  check_options.get("transactionid", ""))
        if state and "." in state:
            transid = state.split(".")[0]
            if "state" in check_options:
                check_options["state"] = transid
            if "transactionid" in check_options:
                check_options["transactionid"] = transid

        # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

        # transaction id optimization - part 1:
        #
        # if we have a transaction id, we check only those tokens
        # that belong to this transaction id:

        challenges = []
        transaction_serials = []
        transid = check_options.get("state",
                                    check_options.get("transactionid", ""))

        # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

        audit_entry = {}
        audit_entry["action_detail"] = "no token found!"

        challenge_tokens = []
        pin_matching_tokens = []
        invalid_tokens = []
        valid_tokens = []
        related_challenges = []

        # we have to preserve the result / reponse for token counters
        validation_results = {}

        for token in tokenList:

            audit_entry["serial"] = token.getSerial()
            audit_entry["token_type"] = token.getType()

            # preselect: the token must be in the same realm as the user
            if user is not None:
                t_realms = token.token.getRealmNames()
                u_realm = user.realm
                if (len(t_realms) > 0 and len(u_realm) > 0
                        and u_realm.lower() not in t_realms):

                    audit_entry[
                        "action_detail"] = "Realm mismatch for token and user"

                    continue

            # check if the token is the list of supported tokens
            # if not skip to the next token in list
            typ = token.getType()
            if typ.lower() not in tokenclass_registry:
                log.error(
                    "token typ %r not found in tokenclasses: %r",
                    typ,
                    list(tokenclass_registry.keys()),
                )
                audit_entry["action_detail"] = "Unknown Token type"
                continue

            if not token.isActive():
                audit_entry["action_detail"] = "Token inactive"
                continue

            if token.getFailCount() >= token.getMaxFailCount():
                audit_entry["action_detail"] = "Failcounter exceeded"
                token.incOtpFailCounter()
                continue

            # ---------------------------------------------------------------------- --

            # check for restricted path usage

            path = context["Path"].strip("/").partition("/")[0]
            token_path = token.getFromTokenInfo("scope", {}).get("path", [])

            if token_path and path not in token_path:
                continue

            # -------------------------------------------------------------- --

            # token validity handling

            if token.is_not_yet_valid():
                msg = "Authentication validity period mismatch!"
                audit_entry["action_detail"] = msg
                token.incOtpFailCounter()
                continue

            if not token.is_valid():
                if token.has_exceeded_usage():
                    msg = "Authentication counter exceeded"
                elif token.has_exceeded_success():
                    msg = "Authentication sucess counter exceeded"
                elif token.is_expired():
                    msg = "Authentication validity period exceeded"
                else:
                    raise Exception("Validity check failed without reason")

                audit_entry["action_detail"] = msg
                token.incOtpFailCounter()

                # what should happen with exceeding tokens

                t_realms = None

                if not user.login and not user.realm:
                    t_realms = token.token.getRealmNames()

                if disable_on_authentication_exceed(user, realms=t_realms):
                    token.enable(False)

                if delete_on_authentication_exceed(user, realms=t_realms):
                    token.deleteToken()

                continue

            # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

            # gather all open challenges for this token
            if transid:
                _expired, challenges = Challenges.get_challenges(
                    token=token, transid=transid, filter_open=True)

            else:
                # if there is no transaction id given we check all challenges
                # related to the given token

                _expired, challenges = Challenges.get_challenges(
                    token=token, filter_open=True, options=check_options)

            # -------------------------------------------------------------- --

            # finally we check the token

            try:
                (ret, reply) = token.check_token(passw,
                                                 user,
                                                 options=check_options,
                                                 challenges=challenges)

            except Exception as exx:
                # in case of a failure during checking token, we log the error
                # and continue with the next one
                log.error("checking token %r failed: %r", token, exx)
                ret = -1
                reply = "%r" % exx
                audit_entry[
                    "action_detail"] = "checking token %r failed: %r" % (token,
                                                                         exx)

                audit_entry["info"] = audit_entry.get("info", "") + "%r" % exx

                continue
            finally:
                validation_results[token.getSerial()] = (ret, reply)

            (cToken, pToken, iToken, vToken) = token.get_verification_result()
            related_challenges.extend(token.related_challenges)

            challenge_tokens.extend(cToken)
            pin_matching_tokens.extend(pToken)
            invalid_tokens.extend(iToken)
            valid_tokens.extend(vToken)

        valid_tokens = list(set(valid_tokens))
        invalid_tokens = list(set(invalid_tokens))
        pin_matching_tokens = list(set(pin_matching_tokens))
        challenge_tokens = list(set(challenge_tokens))

        # end of token verification loop
        matching_challenges = []
        for token in valid_tokens:
            matching_challenges.extend(token.matching_challenges)

        matching_challenges = list(set(matching_challenges))

        # if there are related / sub challenges, we have to call their janitor
        Challenges.handle_related_challenge(matching_challenges)

        # now we finalize the token validation result
        fh = FinishTokens(
            valid_tokens,
            challenge_tokens,
            pin_matching_tokens,
            invalid_tokens,
            validation_results,
            user,
            options,
            audit_entry=audit_entry,
        )

        (res, reply) = fh.finish_checked_tokens()

        # ------------------------------------------------------------------ --

        # add to all tokens the last accessed time stamp

        add_last_accessed_info(
            set(valid_tokens + pin_matching_tokens + challenge_tokens +
                invalid_tokens))

        # add time stamp to all valid tokens

        add_last_verified_info(valid_tokens)

        # ------------------------------------------------------------------ --

        # now we care for all involved tokens and their challenges

        for token in set(valid_tokens + pin_matching_tokens +
                         challenge_tokens + invalid_tokens):
            expired, _valid = Challenges.get_challenges(token)
            if expired:
                Challenges.delete_challenges(None, expired)

        log.debug(
            "Number of valid tokens found (validTokenNum): %d",
            len(valid_tokens),
        )

        return (res, reply)
Example #39
0
    def checkTokenList(self, tokenList, passw, user=User(), options=None):
        """
        identify a matching token and test, if the token is valid, locked ..
        This function is called by checkSerialPass and checkUserPass to

        :param tokenList: list of identified tokens
        :param passw: the provided passw (mostly pin+otp)
        :param user: the identified use - as class object
        :param options: additonal parameters, which are passed to the token

        :return: tuple of boolean and optional response
        """
        log.debug("[__checkTokenList] checking tokenlist: %r" % tokenList)
        reply = None

        tokenclasses = config['tokenclasses']

        #  add the user to the options, so that every token could see the user
        if not options:
            options = {}

        options['user'] = user

        # if there has been one token in challenge mode, we only handle
        # challenges

        # if we got a validation against a sub_challenge, we extend this to
        # be a validation to all challenges of the transaction id
        import copy
        check_options = copy.deepcopy(options)
        state = check_options.get('state',
                                  check_options.get('transactionid', ''))
        if state and '.' in state:
            transid = state.split('.')[0]
            if 'state' in check_options:
                check_options['state'] = transid
            if 'transactionid' in check_options:
                check_options['transactionid'] = transid

        # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

        # transaction id optimization - part 1:
        #
        # if we have a transaction id, we check only those tokens
        # that belong to this transaction id:

        challenges = []
        transaction_serials = []
        transid = check_options.get('state',
                                    check_options.get('transactionid', ''))
        if transid:
            expired, challenges = Challenges.get_challenges(transid=transid,
                                                            filter_open=True)
            for challenge in challenges:
                serial = challenge.tokenserial
                transaction_serials.append(serial)

        # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

        audit_entry = {}
        audit_entry['action_detail'] = "no token found!"

        challenge_tokens = []
        pin_matching_tokens = []
        invalid_tokens = []
        valid_tokens = []
        related_challenges = []

        # we have to preserve the result / reponse for token counters
        validation_results = {}

        for token in tokenList:
            log.debug('Found user with loginId %r: %r:\n', token.getUserId(),
                      token.getSerial())

            # transaction id optimization - part 2:
            if transid:
                if token.getSerial() not in transaction_serials:
                    continue

            audit_entry['serial'] = token.getSerial()
            audit_entry['token_type'] = token.getType()

            # preselect: the token must be in the same realm as the user
            if user is not None:
                t_realms = token.token.getRealmNames()
                u_realm = user.getRealm()
                if (len(t_realms) > 0 and len(u_realm) > 0
                        and u_realm.lower() not in t_realms):

                    audit_entry['action_detail'] = ("Realm mismatch for "
                                                    "token and user")

                    continue

            # check if the token is the list of supported tokens
            # if not skip to the next token in list
            typ = token.getType()
            if typ.lower() not in tokenclasses:
                log.error('token typ %r not found in tokenclasses: %r' %
                          (typ, tokenclasses))
                audit_entry['action_detail'] = "Unknown Token type"
                continue

            if not token.isActive():
                audit_entry['action_detail'] = "Token inactive"
                continue

            if token.getFailCount() >= token.getMaxFailCount():
                audit_entry['action_detail'] = "Failcounter exceeded"
                token.incOtpFailCounter()
                continue

            if not token.check_auth_counter():
                audit_entry[
                    'action_detail'] = "Authentication counter exceeded"
                token.set_count_auth(token.get_count_auth() + 1)
                continue

            if not token.check_validity_period():
                audit_entry['action_detail'] = "validity period mismatch"
                token.incOtpFailCounter()
                continue

            # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

            # start the token validation

            if not transid:
                # if there is no transaction id given we check all token
                # related challenges
                (_ex_challenges,
                 challenges) = Challenges.get_challenges(token,
                                                         options=check_options,
                                                         filter_open=True)

            try:
                (ret, reply) = token.check_token(passw,
                                                 user,
                                                 options=check_options,
                                                 challenges=challenges)
            except Exception as exx:
                # in case of a failure during checking token, we log the error
                # and continue with the next one
                log.exception("checking token %r failed: %r" % (token, exx))
                ret = -1
                reply = "%r" % exx
                audit_entry['action_detail'] = ("checking token %r "
                                                "failed: %r" % (token, exx))
                continue
            finally:
                validation_results[token.getSerial()] = (ret, reply)

            (cToken, pToken, iToken, vToken) = token.get_verification_result()
            related_challenges.extend(token.related_challenges)

            challenge_tokens.extend(cToken)
            pin_matching_tokens.extend(pToken)
            invalid_tokens.extend(iToken)
            valid_tokens.extend(vToken)

        # end of token verification loop
        matching_challenges = []
        for token in valid_tokens:
            matching_challenges.extend(token.matching_challenges)

        # if there are related / sub challenges, we have to call their janitor
        Challenges.handle_related_challenge(matching_challenges)

        # now we finalize the token validation result
        fh = FinishTokens(valid_tokens,
                          challenge_tokens,
                          pin_matching_tokens,
                          invalid_tokens,
                          validation_results,
                          user,
                          options,
                          audit_entry=audit_entry)

        (res, reply) = fh.finish_checked_tokens()

        # add to all tokens the last accessd time stamp
        linotp.lib.token.add_last_accessed_info([
            valid_tokens, pin_matching_tokens, challenge_tokens, valid_tokens
        ])

        # now we care for all involved tokens and their challenges
        for token in (valid_tokens + pin_matching_tokens + challenge_tokens +
                      invalid_tokens):
            expired, _valid = Challenges.get_challenges(token)
            if expired:
                Challenges.delete_challenges(None, expired)

        log.debug("Number of valid tokens found "
                  "(validTokenNum): %d" % len(valid_tokens))

        return (res, reply)
Example #40
0
    def finish_valid_tokens(self):
        """
        processing of the valid tokens
        """
        valid_tokens = self.valid_tokens
        validation_results = self.validation_results
        user = self.user

        if len(valid_tokens) == 1:
            token = valid_tokens[0]
            if user:
                action_detail = ("user %r@%r successfully authenticated."
                                 % (user.login, user.realm))
            else:
                action_detail = ("serial %r successfully authenticated."
                                 % token.getSerial())

            log.info(action_detail)

            # there could be a match in the window ahead,
            # so we need the last valid counter here

            (counter, _reply) = validation_results[token.getSerial()]
            token.setOtpCount(counter + 1)
            token.statusValidationSuccess()

            # finish as well related open challenges
            Challenges.finish_challenges(token, success=True)

            if token.count_auth_success_max > 0:
                token.inc_count_auth_success()

            if token.count_auth_max > 0:
                token.inc_count_auth()

            detail = None
            auth_info = self.options.get('auth_info', 'False')
            if auth_info.lower() == "true":
                detail = token.getAuthDetail()

            # 1. check if token supports offline at all
            supports_offline_at_all = token.supports_offline_mode

            # 2. check if policy allows to use offline authentication
            if user is not None and user.login and user.realm:
                realms = [user.realm]
            else:
                realms = token.getRealms()

            offline_is_allowed = supports_offline(realms, token)

            # 3. check if parameter 'use_offline' is provided
            use_offline_param = self.options.get('use_offline', 'False')
            use_offline = use_offline_param.lower() == 'true'

            if supports_offline_at_all and \
               offline_is_allowed and \
               use_offline:

                offline_info = token.getOfflineInfo()
                if detail is None:
                    detail = {}

                offline = {'serial': token.getSerial(), 'type': token.type}
                offline['offline_info'] = offline_info

                detail.update({'offline': offline})

            janitor_to_remove_enrollment_token(valid_tokens=[token])

            return (True, detail, action_detail)

        else:
            # we have to set the matching counter to prevent replay one one
            # single token

            for token in valid_tokens:

                (res, _reply) = validation_results[token.getSerial()]

                token.setOtpCount(res)

                # in case of multiple matches the tokens were accessed
                # so we count them as well
                if token.count_auth_max > 0:
                    token.inc_count_auth()

            janitor_to_remove_enrollment_token(valid_tokens=valid_tokens)

            context['audit']['action_detail'] = "Multiple valid tokens found!"
            if user:
                log.error("multiple token match error: "
                          "Several Tokens matching with the same OTP PIN "
                          "and OTP for user %r. Not sure how to auth",
                          user.login)

            raise UserError("multiple token match error", id=-33)
Example #41
0
    def resync(self, otp1, otp2, options=None):
        '''
        - for the resync to work, we take the last two transactions and their challenges
        - for each challenge, we search forward the sync window length

        '''
        log.debug('[resync] %r : %r' % (otp1, otp2))

        ret = False
        challenges = []

        ## the challenges are orderd, the first one is the newest
        challenges = Challenges.lookup_challenges(self.context, self.getSerial())

        ##  check if there are enough challenges around
        if len(challenges) < 2:
            return False

        challenge1 = {}
        challenge2 = {}

        if options is None:

            ## the newer one
            ch1 = challenges[0]
            challenge1['challenge'] = ch1.get('data').get('challenge')
            challenge1['transid'] = ch1.get('transid')
            challenge1['session'] = ch1.get('session')
            challenge1['id'] = ch1.get('id')


            ## the elder one
            ch2 = challenges[0]
            challenge2['challenge'] = ch2.get('data').get('challenge')
            challenge2['transid'] = ch2.get('transid')
            challenge2['session'] = ch2.get('session')
            challenge2['id'] = ch2.get('id')

        else:
            if options.has_key('challenge1'):
                challenge1['challenge'] = options.get('challenge1')
            if options.has_key('challenge2'):
                challenge2['challenge'] = options.get('challenge2')


        if len(challenge1) == 0 or len(challenge2) == 0:
            error = "No challeges found!"
            log.error('[Ocra2TokenClass:resync] %s' % (error))
            raise Exception('[Ocra2TokenClass:resync] %s' % (error))



        secretHOtp = self.token.getHOtpKey()
        ocraSuite = OcraSuite(self.getOcraSuiteSuite(), secretHOtp)

        syncWindow = self.token.getSyncWindow()
        if  ocraSuite.T is not None:
            syncWindow = syncWindow / 10

        counter = self.token.getOtpCounter()

        ## set the ocra token pin
        ocraPin = ''
        if ocraSuite.P is not None:
            ocraPinObj = self.token.getUserPin()
            ocraPin = ocraPinObj.getKey()

            if ocraPin is None or len(ocraPin) == 0:
                ocraPin = ''

        timeShift = 0
        if  ocraSuite.T is not None:
            timeShift = int(self.getFromTokenInfo("timeShift", 0))

        try:

            count_1 = ocraSuite.checkOtp(otp1, counter, syncWindow, challenge1, pin=ocraPin, timeshift=timeShift)
            if count_1 == -1:
                log.info('[resync] lookup for first otp value failed!')
                ret = False
            else:
                count_2 = ocraSuite.checkOtp(otp2, counter, syncWindow, challenge2, pin=ocraPin, timeshift=timeShift)
                if count_2 == -1:
                    log.info('[resync] lookup for second otp value failed!')
                    ret = False
                else:
                    if ocraSuite.C is not None:
                        if count_1 + 1 == count_2:
                            self.setOtpCount(count_2)
                            ret = True

                    if  ocraSuite.T is not None:
                        if count_1 - count_2 <= ocraSuite.T * 2:
                            ##  callculate the timeshift
                            date = datetime.datetime.fromtimestamp(count_2)
                            log.info('[resync] syncing token to new timestamp: %r' % (date))

                            now = datetime.datetime.now()
                            stime = now.strftime("%s")
                            timeShift = count_2 - int(stime)
                            self.addToTokenInfo('timeShift', timeShift)
                            ret = True

        except Exception as ex:
            log.exception('[Ocra2TokenClass:resync] unknown error: %r' % (ex))
            raise Exception('[Ocra2TokenClass:resync] unknown error: %s' % (ex))

        log.debug('[resync]: %r ' % (ret))
        return ret
Example #42
0
    def checkTokenList(self, tokenList, passw, user=User(), options=None):
        """
        identify a matching token and test, if the token is valid, locked ..
        This function is called by checkSerialPass and checkUserPass to

        :param tokenList: list of identified tokens
        :param passw: the provided passw (mostly pin+otp)
        :param user: the identified use - as class object
        :param options: additonal parameters, which are passed to the token

        :return: tuple of boolean and optional response
        """
        log.debug("[__checkTokenList] checking tokenlist: %r" % tokenList)
        reply = None

        tokenclasses = config['tokenclasses']

        #  add the user to the options, so that every token could see the user
        if not options:
            options = {}

        options['user'] = user

        # if there has been one token in challenge mode, we only handle challenges

        # if we got a validation against a sub_challenge, we extend this to
        # be a validation to all challenges of the transaction id
        import copy
        check_options = copy.deepcopy(options)
        state = check_options.get('state', check_options.get('transactionid', ''))
        if state and '.' in state:
            transid = state.split('.')[0]
            if 'state' in check_options:
                check_options['state'] = transid
            if 'transactionid' in check_options:
                check_options['transactionid'] = transid

        audit_entry = {}
        audit_entry['action_detail'] = "no token found!"

        challenge_tokens = []
        pin_matching_tokens = []
        invalid_tokens = []
        valid_tokens = []
        related_challenges = []

        # we have to preserve the result / reponse for token counters
        validation_results = {}

        for token in tokenList:
            log.debug('Found user with loginId %r: %r:\n',
                      token.getUserId(), token.getSerial())

            audit_entry['serial'] = token.getSerial()
            audit_entry['token_type'] = token.getType()

            # preselect: the token must be in the same realm as the user
            if user is not None:
                t_realms = token.token.getRealmNames()
                u_realm = user.getRealm()
                if (len(t_realms) > 0 and len(u_realm) > 0 and
                        u_realm.lower() not in t_realms):

                    audit_entry['action_detail'] = ("Realm mismatch for "
                                                    "token and user")

                    continue

            # check if the token is the list of supported tokens
            # if not skip to the next token in list
            typ = token.getType()
            if typ.lower() not in tokenclasses:
                log.error('token typ %r not found in tokenclasses: %r' %
                          (typ, tokenclasses))
                audit_entry['action_detail'] = "Unknown Token type"
                continue

            if not token.isActive():
                audit_entry['action_detail'] = "Token inactive"
                continue
            if token.getFailCount() >= token.getMaxFailCount():
                audit_entry['action_detail'] = "Failcounter exceeded"
                continue
            if not token.check_auth_counter():
                audit_entry['action_detail'] = "Authentication counter exceeded"
                continue
            if not token.check_validity_period():
                audit_entry['action_detail'] = "validity period mismatch"
                continue

            # start the token validation
            try:
                # are there outstanding challenges
                (_ex_challenges,
                 challenges) = Challenges.get_challenges(token,
                                                         options=check_options)

                (ret, reply) = token.check_token(
                    passw, user, options=check_options, challenges=challenges)
            except Exception as exx:
                # in case of a failure during checking token, we log the error
                # and continue with the next one
                log.exception("checking token %r failed: %r" % (token, exx))
                ret = -1
                reply = "%r" % exx
                audit_entry['action_detail'] = ("checking token %r "
                                                "failed: %r" % (token, exx))
                continue
            finally:
                validation_results[token.getSerial()] = (ret, reply)

            (cToken, pToken, iToken, vToken) = token.get_verification_result()
            related_challenges.extend(token.related_challenges)

            challenge_tokens.extend(cToken)
            pin_matching_tokens.extend(pToken)
            invalid_tokens.extend(iToken)
            valid_tokens.extend(vToken)

        # end of token verification loop

        # if there are related / sub challenges, we have to call their janitor
        Challenges.handle_related_challenge(related_challenges)

        # now we finalize the token validation result
        fh = FinishTokens(valid_tokens,
                          challenge_tokens,
                          pin_matching_tokens,
                          invalid_tokens,
                          validation_results,
                          user, options,
                          audit_entry=audit_entry)

        (res, reply) = fh.finish_checked_tokens()

        # add to all tokens the last accessd time stamp
        linotp.lib.token.add_last_accessed_info(
            [valid_tokens, pin_matching_tokens, challenge_tokens, valid_tokens])

        # now we care for all involved tokens and their challenges
        for token in (valid_tokens + pin_matching_tokens +
                      challenge_tokens + valid_tokens):
            expired, _valid = Challenges.get_challenges(token)
            if expired:
                Challenges.delete_challenges(None, expired)

        log.debug("Number of valid tokens found "
                  "(validTokenNum): %d" % len(valid_tokens))

        return (res, reply)
Example #43
0
    def checkResponse4Challenge(self, user, passw, options=None,
                                challenges=None):
        """
        verify the response of a previous challenge

        There are two possible cases:

        1) The 'transaction_id' (also know as 'state', which has the same
           value) is available in options
        2) No 'transaction_id'

        In the first case we can safely assume that the passw only contains
        the OTP (no pin). In the second case passw will contain both and we
        split to get the OTP.

        :param user:     the requesting user
        :param passw:    the to be checked pass (pin+otp)
        :param options:  options an additional argument, which could be token
                          specific
        :param challenges: the list of challenges, where each challenge is
                            described as dict
        :return: tuple of (otpcounter and the list of matching challenges)

        """
        transaction_id = None
        otp_counter = -1
        matching_challenges = []

        if challenges is None or len(challenges) == 0:
            # There are no challenges for this token
            return -1, []

        if options and ('transactionid' in options or 'state' in options):
            ## fetch the transactionid
            transaction_id = options.get('transactionid', None)
            if transaction_id is None:
                transaction_id = options.get('state', None)

        if transaction_id:
            otp = passw
            # if the transaction_id is set we can assume that we have only
            # received a single challenge with that transaction_id thanks to
            # linotp.lib.validate.ValidateToken.get_challenges()
            assert(len(challenges) == 1)
            assert(Challenges.is_same_transaction(challenges[0], transaction_id))
        else:
            # If no transaction_id is set the request came through the WebUI
            # and we have to check all challenges
            split_status, _, otp = split_pin_otp(self, passw, user, options)
            if split_status < 0:
                raise Exception("Could not split passw")

        window = self.getOtpCountWindow()

        for challenge in challenges:
            challenge_data = challenge.getData()
            stored_counter = challenge_data.get("counter_value")
            temp_otp_counter = self.checkOtp(otp, int(stored_counter),
                                             window, options)
            if temp_otp_counter > 0:
                otp_counter = temp_otp_counter
                matching_challenges = [challenge]
                break

        # The matching_challenges list will either contain a single challenge
        # or will be empty. Returning multiple challenges is not useful in this
        # case because all older challenges arecleaned up anyway.
        return otp_counter, matching_challenges
Example #44
0
    def update(self, params):

        param_keys = set(params.keys())
        init_rollout_state_keys = set(['type', 'hashlib', 'serial',
                                       'key_size', 'user.login',
                                       'user.realm', 'session'])

        # ----------------------------------------------------------------------

        if param_keys.issubset(init_rollout_state_keys):

            # if param keys are in {'type', 'hashlib'} the token is
            # initialized for the first time. this is e.g. done on the
            # manage web ui. since the token doesn't exist in the database
            # yet, its rollout state must be None (that is: they data for
            # the rollout state doesn't exist yet)

            self.ensure_state(None)

            # ------------------------------------------------------------------

            # collect data used for generating the pairing url

            serial = params.get('serial')
            hash_algorithm = params.get('hashlib')
            pub_key = get_qrtoken_public_key()

            cb_url = get_single_auth_policy('qrtoken_pairing_callback_url')
            cb_sms = get_single_auth_policy('qrtoken_pairing_callback_sms')

            # TODO: read from config
            otp_pin_length = None

            # ------------------------------------------------------------------

            pairing_url = generate_pairing_url('qrtoken',
                                          server_public_key=pub_key,
                                          serial=serial,
                                          callback_url=cb_url,
                                          callback_sms_number=cb_sms,
                                          otp_pin_length=otp_pin_length,
                                          hash_algorithm=hash_algorithm)

            # ------------------------------------------------------------------

            self.addToInfo('pairing_url', pairing_url)

            # we set the the active state of the token to False, because
            # it should not be allowed to use it for validation before the
            # pairing process is done

            self.token.LinOtpIsactive = False

            # ------------------------------------------------------------------

            self.change_state('pairing_url_sent')

        # ----------------------------------------------------------------------

        elif 'pairing_response' in params:

            # if a pairing response is in the parameters, we guess,
            # that the request refers to a token in the state
            # 'pairing_url_sent'

            self.ensure_state('pairing_url_sent')

            # ------------------------------------------------------------------

            # adding the user's public key to the token info
            # as well as the user_token_id, which is used to
            # identify the token on the user's side

            self.addToTokenInfo('user_token_id', params['user_token_id'])

            # user public key arrives in the bytes format.
            # we must convert to a string in order to be
            # able to dump it as json in the db

            b64_user_public_key = b64encode(params['user_public_key'])
            self.addToTokenInfo('user_public_key', b64_user_public_key)

            # ------------------------------------------------------------------

            # create challenge through the challenge factory

            # add the content type and the challenge data to the params
            # (needed in the createChallenge method)

            params['content_type'] = CONTENT_TYPE_PAIRING
            params['data'] = self.getSerial()

            self.change_state('pairing_response_received')

            success, challenge_dict = Challenges.create_challenge(self, params)

            if not success:
                raise Exception('Unable to create challenge from '
                                'pairing response %s' %
                                params['pairing_response'])

            challenge_url = challenge_dict['message']

            # ------------------------------------------------------------------

            self.addToInfo('pairing_challenge_url', challenge_url)

            # ------------------------------------------------------------------

            self.change_state('pairing_challenge_sent')
Example #45
0
    def _rollout_2(self, params):
        '''
        2.

        https://linotpserver/admin/init?
            type=ocra&
            genkey=1&
            activationcode=AKTIVIERUNGSCODE&
            user=BENUTZERNAME&
            message=MESSAGE&
            session=SESSIONKEY

        =>> "serial" : SERIENNUMMER, "nonce" : DATAOBJECT, "transactionid" : "TRANSAKTIONSID, "app_import" : IMPORTURL

        - nonce - von HSM oder random ?
        - pkcs5 - kdf2
        - es darf zur einer Zeit nur eine QR Token inaktiv (== im Ausrollzustand) sein !!!!!
          der Token wird über den User gefunden
        - seed = pdkdf2(nonce + activcode + shared secret)
        - challenge generiern - von urandom oder HSM

        '''
        log.debug('[_rollout_2] %r ' % (params))

        activationcode = params.get('activationcode', None)
        if activationcode is not None:

            ##  genkey might have created a new key, so we have to rely on
            encSharedSecret = self.getFromTokenInfo('sharedSecret', None)
            if encSharedSecret is None:
                raise Exception ('missing shared secret of initialition for token %r' % (self.getSerial()))

            sharedSecret = decryptPin(encSharedSecret)

            ##  we generate a nonce, which in the end is a challenge
            nonce = createNonce()
            self.addToTokenInfo('nonce', nonce)

            ##  create a new key from the ocrasuite
            key_len = 20
            if self.ocraSuite.find('-SHA256'):
                key_len = 32
            elif self.ocraSuite.find('-SHA512'):
                key_len = 64


            newkey = kdf2(sharedSecret, nonce, activationcode, key_len)
            self.setOtpKey(binascii.hexlify(newkey))

            ##  generate challenge, which is part of the app_import
            message = params.get('message', None)

            #(transid, challenge, _ret, url) = self.challenge(message)

            #self.createChallenge()
            (res, opt) = Challenges.create_challenge(self, self.context, options=params)

            challenge = opt.get('challenge')
            url = opt.get('url')
            transid = opt.get('transactionid')

            ##  generate response
            info = {}
            uInfo = {}
            info['serial'] = self.getSerial()
            uInfo['se'] = self.getSerial()
            info['nonce'] = nonce
            uInfo['no'] = nonce
            info['transactionid'] = transid
            uInfo['tr'] = transid
            info['challenge'] = challenge
            uInfo['ch'] = challenge
            if message is not None:
                uInfo['me'] = str(message.encode("utf-8"))

            ustr = urllib.urlencode({'u':str(url.encode("utf-8"))})
            uInfo['u'] = ustr[2:]
            info['url'] = str(url.encode("utf-8"))

            app_import = 'lseqr://nonce?%s' % (urllib.urlencode(uInfo))

            ##  add a signature of the url
            signature = {'si': self.signData(app_import) }
            info['signature'] = signature.get('si')

            info['app_import'] = "%s&%s" % (app_import, urllib.urlencode(signature))
            self.info = info

            ##  setup new state
            self.addToTokenInfo('rollout', '2')
            self.enable(True)

        log.debug('[_rollout_2]:')
        return
Example #46
0
    def check_status(self, transid=None, user=None, serial=None,
                     password=None, use_offline=False):
        """
        check for open transactions - for polling support

        :param transid: the transaction id where we request the status from
        :param user: the token owner user
        :param serial: or the serial we are searching for
        :param password: the pin/password for authorization the request
        :param use_offline: on success the offline info is returned

        :return: tuple of success and detail dict
        """

        expired, challenges = Challenges.get_challenges(None, transid=transid)

        # remove all expired challenges
        if expired:
            Challenges.delete_challenges(None, expired)

        if not challenges:
            return False, None

        # there is only one challenge per transaction id
        # if not multiple challenges, where transaction id is the parent one
        reply = {}

        pin_policies = linotp.lib.policy.get_pin_policies(user)
        if 1 in pin_policies:
            pin_match = check_pin(None, password, user=user, options=None)
            if not pin_match:
                return False, None

        involved_tokens = []

        transactions = {}
        for ch in challenges:

            # only look for challenges that are not compromised
            if not Challenges.verify_checksum(ch):
                continue

            # is the requester authorized
            serial = ch.getTokenSerial()
            tokens = getTokens4UserOrSerial(serial=serial)
            if not tokens:
                continue
            involved_tokens.extend(tokens)

            # as one challenge belongs exactly to only one token,
            # we take this one as the token
            token = tokens[0]

            if 1 not in pin_policies:
                pin_match = check_pin(token, password, user=user,
                                      options=None)
                if not pin_match:
                    ret = False
                    continue

            ret = True

            trans_dict = {}

            trans_dict['received_count'] = ch.received_count
            trans_dict['received_tan'] = ch.received_tan
            trans_dict['valid_tan'] = ch.valid_tan
            trans_dict['message'] = ch.challenge
            trans_dict['status'] = ch.getStatus()

            token_dict = {'serial': serial, 'type': token.type}

            # 1. check if token supports offline at all
            supports_offline_at_all = token.supports_offline_mode

            # 2. check if policy allows to use offline authentication
            if user is not None and user.login and user.realm:
                realms = [user.realm]
            else:
                realms = token.getRealms()

            offline_is_allowed = supports_offline(realms, token)

            if not ch.is_open() and ch.valid_tan and \
               supports_offline_at_all and \
               offline_is_allowed and \
               use_offline:
                token_dict['offline_info'] = token.getOfflineInfo()

            trans_dict['token'] = token_dict
            transactions[ch.transid] = trans_dict

        if transactions:
            reply['transactions'] = transactions

        return ret, reply
Example #47
0
    def checkTokenList(self, tokenList, passw, user=User(), options=None):
        """
        identify a matching token and test, if the token is valid, locked ..
        This function is called by checkSerialPass and checkUserPass to

        :param tokenList: list of identified tokens
        :param passw: the provided passw (mostly pin+otp)
        :param user: the identified use - as class object
        :param options: additional parameters, which are passed to the token

        :return: tuple of boolean and optional response
        """
        reply = None

        #  add the user to the options, so that every token could see the user
        if not options:
            options = {}

        options['user'] = user

        # if there has been one token in challenge mode, we only handle
        # challenges

        # if we got a validation against a sub_challenge, we extend this to
        # be a validation to all challenges of the transaction id
        import copy
        check_options = copy.deepcopy(options)
        state = check_options.get(
            'state', check_options.get('transactionid', ''))
        if state and '.' in state:
            transid = state.split('.')[0]
            if 'state' in check_options:
                check_options['state'] = transid
            if 'transactionid' in check_options:
                check_options['transactionid'] = transid

        # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

        # transaction id optimization - part 1:
        #
        # if we have a transaction id, we check only those tokens
        # that belong to this transaction id:

        challenges = []
        transaction_serials = []
        transid = check_options.get('state',
                                    check_options.get('transactionid', ''))
        if transid:
            expired, challenges = Challenges.get_challenges(transid=transid,
                                                            filter_open=True)
            for challenge in challenges:
                serial = challenge.tokenserial
                transaction_serials.append(serial)

        # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

        audit_entry = {}
        audit_entry['action_detail'] = "no token found!"

        challenge_tokens = []
        pin_matching_tokens = []
        invalid_tokens = []
        valid_tokens = []
        related_challenges = []

        # we have to preserve the result / reponse for token counters
        validation_results = {}

        for token in tokenList:

            # transaction id optimization - part 2:
            if transid:
                if token.getSerial() not in transaction_serials:
                    continue

            audit_entry['serial'] = token.getSerial()
            audit_entry['token_type'] = token.getType()

            # preselect: the token must be in the same realm as the user
            if user is not None:
                t_realms = token.token.getRealmNames()
                u_realm = user.realm
                if (len(t_realms) > 0 and len(u_realm) > 0 and
                        u_realm.lower() not in t_realms):

                    audit_entry['action_detail'] = ("Realm mismatch for "
                                                    "token and user")

                    continue

            # check if the token is the list of supported tokens
            # if not skip to the next token in list
            typ = token.getType()
            if typ.lower() not in tokenclass_registry:
                log.error('token typ %r not found in tokenclasses: %r' %
                          (typ, tokenclass_registry.keys()))
                audit_entry['action_detail'] = "Unknown Token type"
                continue

            if not token.isActive():
                audit_entry['action_detail'] = "Token inactive"
                continue

            if token.getFailCount() >= token.getMaxFailCount():
                audit_entry['action_detail'] = "Failcounter exceeded"
                token.incOtpFailCounter()
                continue

            # ---------------------------------------------------------------------- --

            # check for restricted path usage

            path = context['Path'].strip('/').partition('/')[0]
            token_path = token.getFromTokenInfo('scope', {}).get('path', [])

            if token_path and path not in token_path:
                continue

            # -------------------------------------------------------------- --

            # token validity handling

            now = datetime.now()

            if (token.validity_period_start and
                now < token.validity_period_start):

                audit_entry['action_detail'] = ("Authentication validity "
                                                "period mismatch!")

                token.incOtpFailCounter()

                continue

            token_success_excceed = (
                token.count_auth_success_max > 0 and
                token.count_auth_success >= token.count_auth_success_max)

            token_access_exceed = (
                token.count_auth_max > 0 and
                token.count_auth >= token.count_auth_max)

            token_expiry = (
                token.validity_period_end and
                now >= token.validity_period_end)

            if token_success_excceed or token_access_exceed or token_expiry:

                if token_access_exceed:
                    msg = "Authentication counter exceeded"

                if token_success_excceed:
                    msg = "Authentication sucess counter exceeded"

                if token_expiry:
                    msg = "Authentication validity period exceeded!"

                audit_entry['action_detail'] = msg

                token.incOtpFailCounter()

                # what should happen with exceeding tokens

                t_realms = None

                if not user.login and not user.realm:
                    t_realms = token.token.getRealmNames()

                if disable_on_authentication_exceed(user, realms=t_realms):
                    token.enable(False)

                if delete_on_authentication_exceed(user, realms=t_realms):
                    token.deleteToken()

                continue

            # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

            # start the token validation

            if not transid:
                # if there is no transaction id given we check all token
                # related challenges
                (_ex_challenges,
                 challenges) = Challenges.get_challenges(token,
                                                         options=check_options,
                                                         filter_open=True)

            try:
                (ret, reply) = token.check_token(
                    passw, user, options=check_options, challenges=challenges)
            except Exception as exx:
                # in case of a failure during checking token, we log the error
                # and continue with the next one
                log.exception("checking token %r failed: %r" % (token, exx))
                ret = -1
                reply = "%r" % exx
                audit_entry['action_detail'] = ("checking token %r "
                                                "failed: %r" % (token, exx))

                audit_entry['info'] = audit_entry.get('info','') + "%r" % exx

                continue
            finally:
                validation_results[token.getSerial()] = (ret, reply)

            (cToken, pToken, iToken, vToken) = token.get_verification_result()
            related_challenges.extend(token.related_challenges)

            challenge_tokens.extend(cToken)
            pin_matching_tokens.extend(pToken)
            invalid_tokens.extend(iToken)
            valid_tokens.extend(vToken)

        # end of token verification loop
        matching_challenges = []
        for token in valid_tokens:
            matching_challenges.extend(token.matching_challenges)

        # if there are related / sub challenges, we have to call their janitor
        Challenges.handle_related_challenge(matching_challenges)

        # now we finalize the token validation result
        fh = FinishTokens(valid_tokens,
                          challenge_tokens,
                          pin_matching_tokens,
                          invalid_tokens,
                          validation_results,
                          user, options,
                          audit_entry=audit_entry)

        (res, reply) = fh.finish_checked_tokens()

        # add to all tokens the last accessd time stamp
        add_last_accessed_info(
            [valid_tokens, pin_matching_tokens, challenge_tokens, valid_tokens])

        # now we care for all involved tokens and their challenges
        for token in (valid_tokens + pin_matching_tokens +
                      challenge_tokens + invalid_tokens):
            expired, _valid = Challenges.get_challenges(token)
            if expired:
                Challenges.delete_challenges(None, expired)

        log.debug("Number of valid tokens found "
                  "(validTokenNum): %d" % len(valid_tokens))

        return (res, reply)
Example #48
0
    def check_status(self, transid=None, user=None, serial=None,
                     password=None, use_offline=False):
        """
        check for open transactions - for polling support

        :param transid: the transaction id where we request the status from
        :param user: the token owner user
        :param serial: or the serial we are searching for
        :param password: the pin/password for authorization the request
        :param use_offline: on success the offline info is returned

        :return: tuple of success and detail dict
        """

        expired, challenges = Challenges.get_challenges(token=None, transid=transid)

        # remove all expired challenges
        if expired:
            Challenges.delete_challenges(None, expired)

        if not challenges:
            return False, None

        # there is only one challenge per transaction id
        # if not multiple challenges, where transaction id is the parent one
        reply = {}
        involved_tokens = []

        transactions = {}
        for ch in challenges:

            # only look for challenges that are not compromised
            if not Challenges.verify_checksum(ch):
                continue

            # is the requester authorized
            challenge_serial = ch.getTokenSerial()
            if serial and challenge_serial != serial:
                continue

            tokens = getTokens4UserOrSerial(serial=challenge_serial)
            if not tokens:
                continue

            involved_tokens.extend(tokens)

            # as one challenge belongs exactly to only one token,
            # we take this one as the token
            token = tokens[0]
            owner = get_token_owner(token)

            if user and user != owner:
                continue

            involved_tokens.extend(tokens)

            # we only check the user password / token pin if the user
            # paranmeter is given

            if user and owner:
                pin_match = check_pin(token, password, user=owner,
                                       options=None)
            else:
                pin_match = token.checkPin(password)

            if not pin_match:
                continue

            trans_dict = {}

            trans_dict['received_count'] = ch.received_count
            trans_dict['received_tan'] = ch.received_tan
            trans_dict['valid_tan'] = ch.valid_tan
            trans_dict['message'] = ch.getChallenge()
            trans_dict['status'] = ch.getStatus()

            # -------------------------------------------------------------- --

            # extend the check status with the accept or deny of a transaction

            challenge_session = ch.getSession()

            if challenge_session:

                challenge_session_dict = json.loads(challenge_session)

                if 'accept' in challenge_session_dict:
                    trans_dict['accept'] = challenge_session_dict['accept']

                if 'reject' in challenge_session_dict:
                    trans_dict['reject'] = challenge_session_dict['reject']

            # -------------------------------------------------------------- --

            token_dict = {'serial': token.getSerial(), 'type': token.type}

            # 1. check if token supports offline at all
            supports_offline_at_all = token.supports_offline_mode

            # 2. check if policy allows to use offline authentication
            if user is not None and user.login and user.realm:
                realms = [user.realm]
            else:
                realms = token.getRealms()

            offline_is_allowed = supports_offline(realms, token)

            if not ch.is_open() and ch.valid_tan and \
               supports_offline_at_all and \
               offline_is_allowed and \
               use_offline:
                token_dict['offline_info'] = token.getOfflineInfo()

            trans_dict['token'] = token_dict
            transactions[ch.transid] = trans_dict

        if transactions:
            reply['transactions'] = transactions

        return len(reply) > 0, reply
Example #49
0
    def checkOtp(self, passwd, counter, window, options=None):
        """
        checks if the supplied challenge response is correct.

        :param passwd: The challenge response

        :param options: A dictionary of parameters passed by the upper
            layer (used for transaction_id in this context)

        :param counter: legacy API (unused)

        :param window: legacy API (unused)

        :raises TokenStateError: If token state is not 'active' or
            'pairing_challenge_sent'

        :returns: -1 for failure, 1 for success
        """

        valid_states = ['pairing_challenge_sent', 'active']

        self.ensure_state_is_in(valid_states)

        # ------------------------------------------------------------------ --

        # new pushtoken protocoll supports the keyword based accept or deny.
        # the old 'passwd' argument is not supported anymore

        try:

            signature_accept = passwd.get('accept', None)
            signature_reject = passwd.get('reject', None)

        except AttributeError:  # will be raised with a get() on a str object

            raise Exception('Pushtoken version %r requires "accept" or'
                            ' "reject" as parameter' % CHALLENGE_URL_VERSION)

        if signature_accept is not None and signature_reject is not None:

            raise Exception('Pushtoken version %r requires "accept" or'
                            ' "reject" as parameter' % CHALLENGE_URL_VERSION)

        # ------------------------------------------------------------------ --

        filtered_challenges = []
        serial = self.getSerial()

        if options is None:
            options = {}

        max_fail = int(getFromConfig('PushMaxChallenges', '3'))

        # ------------------------------------------------------------------ --

        if 'transactionid' in options:

            # -------------------------------------------------------------- --

            # fetch all challenges that match the transaction id or serial

            transaction_id = options.get('transactionid')

            challenges = Challenges.lookup_challenges(serial=serial,
                                                      transid=transaction_id,
                                                      filter_open=True)

            # -------------------------------------------------------------- --

            # filter into filtered_challenges

            for challenge in challenges:

                (received_tan, tan_is_valid) = challenge.getTanStatus()
                fail_counter = challenge.getTanCount()

                # if we iterate over matching challenges (that is: challenges
                # with the correct transaction id) we either find a fresh
                # challenge, that didn't receive a TAN at all (first case)
                # or a challenge, that already received a number of wrong
                # TANs but still has tries left (second case).

                if not received_tan:
                    filtered_challenges.append(challenge)
                elif not tan_is_valid and fail_counter <= max_fail:
                    filtered_challenges.append(challenge)

        # ------------------------------------------------------------------ --

        if not filtered_challenges:
            return -1

        if len(filtered_challenges) > 1:
            log.error('multiple challenges for one transaction and for one'
                      ' token found!')
            return -1

        # for the serial and the transaction id there could always be only
        # at max one challenge matching. This is even true for sub transactions

        challenge = filtered_challenges[0]

        # client verifies the challenge by signing the challenge
        # plaintext. we retrieve the original plaintext (saved
        # in createChallenge) and check for a match

        data = challenge.getData()
        data_to_verify = b64decode(data['sig_base'])

        b64_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key')
        user_dsa_public_key = b64decode(b64_dsa_public_key)

        # -------------------------------------------------------------- --

        # handle the accept case

        if signature_accept is not None:

            accept_signature_as_bytes = decode_base64_urlsafe(signature_accept)

            accept_data_to_verify_as_bytes = (
                struct.pack('<b', CHALLENGE_URL_VERSION) + b'ACCEPT\0' +
                data_to_verify)

            try:
                verify_sig(accept_signature_as_bytes,
                           accept_data_to_verify_as_bytes, user_dsa_public_key)

                challenge.add_session_info({'accept': True})

                return 1

            except ValueError:

                challenge.add_session_info({'accept': False})
                log.error("accept signature mismatch!")

                return -1

        # -------------------------------------------------------------- --

        # handle the reject case

        elif signature_reject is not None:

            reject_signature_as_bytes = decode_base64_urlsafe(signature_reject)

            reject_data_to_verify_as_bytes = (
                struct.pack('<b', CHALLENGE_URL_VERSION) + b'DENY\0' +
                data_to_verify)

            try:
                verify_sig(reject_signature_as_bytes,
                           reject_data_to_verify_as_bytes, user_dsa_public_key)

                challenge.add_session_info({'reject': True})

                return 1

            except ValueError:

                challenge.add_session_info({'reject': False})
                log.error("reject signature mismatch!")

                return -1

        return -1
Example #50
0
    def check_status(self):
        """
        check the status of a transaction - for polling support
        """

        try:

            param = self.request_params

            #
            # we require either state or transactionid as parameter

            transid = param.get("state", param.get("transactionid", None))
            if not transid:
                raise ParameterError(
                    _(
                        'Missing required parameter "state" or '
                        '"transactionid"!'
                    )
                )

            #
            # serial is an optional parameter

            serial = param.get("serial", None)

            # user is an optional parameter:
            # if no 'user' in the parameters, the User object will be empty
            user = getUserFromParam(param)

            passw = param.get("pass")
            if passw is None:
                raise ParameterError(_('Missing required parameter "pass"!'))

            use_offline = param.get("use_offline", False)

            va = ValidationHandler()
            ok, opt = va.check_status(
                transid=transid,
                user=user,
                serial=serial,
                password=passw,
                use_offline=use_offline,
            )

            serials = []
            types = []
            owner = None
            challenges = Challenges.lookup_challenges(transid=transid)

            for ch in challenges:
                tokens = getTokens4UserOrSerial(serial=ch.getTokenSerial())
                if not tokens:
                    continue

                for token in tokens:
                    serials.append(token.getSerial())
                    types.append(token.getType())

                    if not owner:
                        owner = get_token_owner(token)

            if owner:
                g.audit["user"] = g.audit["user"] or owner.login
                g.audit["realm"] = g.audit["realm"] or owner.realm

            g.audit["serial"] = " ".join(serials)
            g.audit["token_type"] = " ".join(types)

            g.audit["success"] = ok
            g.audit["info"] = str(opt)

            db.session.commit()
            return sendResult(response, ok, 0, opt=opt)

        except Exception as exx:
            log.error("check_status failed: %r", exx)
            g.audit["info"] = str(exx)
            db.session.rollback()
            return sendResult(response, False, 0)
Example #51
0
    def check_status(self, transid=None, user=None, serial=None,
                     password=None):
        """
        check for open transactions - for polling support

        :param transid: the transaction id where we request the status from
        :param user: the token owner user
        :param serial: or the serial we are searching for
        :param password: the pin/password for authorization the request

        :return: tuple of success and detail dict
        """

        expired, challenges = Challenges.get_challenges(None, transid=transid)

        # remove all expired challenges
        if expired:
            Challenges.delete_challenges(None, expired)

        if not challenges:
            return False, None

        # there is only one challenge per transaction id
        # if not multiple challenges, where transaction id is the parent one
        reply = {}

        pin_policies = linotp.lib.policy.get_pin_policies(user)
        if 1 in pin_policies:
            pin_match = check_pin(None, password, user=user, options=None)
            if not pin_match:
                return False, None

        involved_tokens = []

        transactions = {}
        for ch in challenges:

            # only look for challenges that are not compromised
            if not Challenges.verify_checksum(ch):
                continue

            # is the requester authorized
            serial = ch.getTokenSerial()
            tokens = getTokens4UserOrSerial(serial=serial)
            if not tokens:
                continue
            involved_tokens.extend(tokens)

            if 1 not in pin_policies:
                pin_match = check_pin(tokens[0], password, user=user,
                                      options=None)
                if not pin_match:
                    ret = False
                    continue

            ret = True

            trans_dict = {}
            trans_dict['transactionid'] = ch.transid
            trans_dict['received_count'] = ch.received_count
            trans_dict['received_tan'] = ch.received_tan
            trans_dict['valid_tan'] = ch.valid_tan
            trans_dict['linotp_tokenserial'] = serial
            trans_dict['linotp_tokentype'] = tokens[0].type
            trans_dict['message'] = ch.challenge

            transactions[serial] = trans_dict

        if transactions:
            reply['transactions'] = transactions

        return ret, reply
Example #52
0
    def checkOtp(self, passwd, counter, window, options=None):

        valid_states = ['pairing_challenge_sent', 'pairing_complete']

        self.ensure_state_is_in(valid_states)

        # ------------------------------------------------------------------- --

        filtered_challenges = []
        serial = self.getSerial()

        if options is None:
            options = {}

        max_fail = int(getFromConfig('QRMaxChallenges', '3'))

        # ------------------------------------------------------------------- --

        # TODO: from which point is checkOtp called, when there
        # is no challenge response in the request?

        if 'transactionid' in options:

            # --------------------------------------------------------------- --

            # fetch all challenges that match the transaction id or serial

            transaction_id = options.get('transaction_id')

            challenges = Challenges.lookup_challenges(serial, transaction_id)

            # --------------------------------------------------------------- --

            # filter into filtered_challenges

            for challenge in challenges:

                (received_tan, tan_is_valid) = challenge.getTanStatus()
                fail_counter = challenge.getTanCount()

                # if we iterate over matching challenges (that is: challenges
                # with the correct transaction id) we either find a fresh
                # challenge, that didn't receive a TAN at all (first case)
                # or a challenge, that already received a number of wrong
                # TANs but still has tries left (second case).

                if not received_tan:
                    filtered_challenges.append(challenge)
                elif not tan_is_valid and fail_counter <= max_fail:
                    filtered_challenges.append(challenge)

            # --------------------------------------------------------------- --

        if not filtered_challenges:
            return -1

        for challenge in filtered_challenges:

            data = challenge.getData()
            correct_passwd = data['user_sig']

            # compare values with python's native constant
            # time comparison

            if compare_digest(correct_passwd, passwd):

                return 1

            else:

                # maybe we got a tan instead of a signature

                correct_passwd_as_bytes = decode_base64_urlsafe(correct_passwd)
                tan_length = 8  # TODO fetch from policy
                correct_tan = extract_tan(correct_passwd_as_bytes, tan_length)

                # TODO PYLONS-HACK pylons silently converts integers in
                # incoming json to unicode. since extract_tan returns
                # an integer, we have to convert it here
                correct_tan = unicode(correct_tan)

                if compare_digest(correct_tan, passwd):
                    return 1

        return -1  # TODO: ??? semantics of this ret val?
Example #53
0
    def checkOtp(self, passwd, counter, window, options=None):

        """
        checks if the supplied challenge response is correct.

        :param passwd: The challenge response

        :param options: A dictionary of parameters passed by the upper
            layer (used for transaction_id in this context)

        :param counter: legacy API (unused)

        :param window: legacy API (unused)

        :raises TokenStateError: If token state is not 'active' or
            'pairing_challenge_sent'

        :returns: -1 for failure, 1 for success
        """

        valid_states = ['pairing_challenge_sent',
                        'active']

        self.ensure_state_is_in(valid_states)

        # ------------------------------------------------------------------- --

        filtered_challenges = []
        serial = self.getSerial()

        if options is None:
            options = {}

        max_fail = int(getFromConfig('PushMaxChallenges', '3'))

        # ------------------------------------------------------------------- --

        if 'transactionid' in options:

            # --------------------------------------------------------------- --

            # fetch all challenges that match the transaction id or serial

            transaction_id = options.get('transaction_id')

            challenges = Challenges.lookup_challenges(serial, transaction_id)

            # --------------------------------------------------------------- --

            # filter into filtered_challenges

            for challenge in challenges:

                (received_tan, tan_is_valid) = challenge.getTanStatus()
                fail_counter = challenge.getTanCount()

                # if we iterate over matching challenges (that is: challenges
                # with the correct transaction id) we either find a fresh
                # challenge, that didn't receive a TAN at all (first case)
                # or a challenge, that already received a number of wrong
                # TANs but still has tries left (second case).

                if not received_tan:
                    filtered_challenges.append(challenge)
                elif not tan_is_valid and fail_counter <= max_fail:
                    filtered_challenges.append(challenge)

        # ------------------------------------------------------------------- --

        if not filtered_challenges:
            return -1

        for challenge in filtered_challenges:

            # client verifies the challenge by signing the challenge
            # plaintext. we retrieve the original plaintext (saved
            # in createChallenge) and check for a match

            b64_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key')
            user_dsa_public_key = b64decode(b64_dsa_public_key)

            data = challenge.getData()
            sig_base = data['sig_base']

            passwd_as_bytes = decode_base64_urlsafe(passwd)
            sig_base_as_bytes = b64decode(sig_base)
            try:
                verify_sig(passwd_as_bytes,
                           sig_base_as_bytes,
                           user_dsa_public_key)
                return 1
            except ValueError:  # signature mismatch
                return -1

        return -1
Example #54
0
    def check_by_transactionid(self, transid, passw, options=None):
        """
        check the passw against the open transaction

        :param transid: the transaction id
        :param passw: the pass parameter
        :param options: the additional optional parameters

        :return: tuple of boolean and detail dict
        """
        reply = {}

        serials = []
        challenges = Challenges.lookup_challenges(transid=transid)

        for challenge in challenges:
            serials.append(challenge.tokenserial)

        if not serials:
            reply["value"] = False
            reply["failure"] = ("No challenge for transaction %r found" %
                                transid)

            return False, reply

        ok = False
        reply["failcount"] = 0
        reply["value"] = False
        reply["token_type"] = ""

        token_type = options.get("token_type", None)

        for serial in serials:

            tokens = getTokens4UserOrSerial(serial=serial,
                                            token_type=token_type,
                                            read_for_update=True)

            if not tokens and token_type:
                continue

            if not tokens and not token_type:
                raise Exception("tokenmismatch for token serial: %r" % serial)

            # there could be only one
            token = tokens[0]
            owner = get_token_owner(token)

            (ok, opt) = self.checkTokenList(tokens,
                                            passw,
                                            user=owner,
                                            options=options)
            if opt:
                reply.update(opt)

            reply["value"] = ok
            reply["token_type"] = token.getType()
            reply["failcount"] = token.getFailCount()
            reply["serial"] = token.getSerial()

            if ok:
                break

        return ok, reply
Example #55
0
    def checkTokenList(self, tokenList, passw, user=User(), options=None):
        """
        identify a matching token and test, if the token is valid, locked ..
        This function is called by checkSerialPass and checkUserPass to

        :param tokenList: list of identified tokens
        :param passw: the provided passw (mostly pin+otp)
        :param user: the identified use - as class object
        :param options: additional parameters, which are passed to the token

        :return: tuple of boolean and optional response
        """
        reply = None

        #  add the user to the options, so that every token could see the user
        if not options:
            options = {}

        options['user'] = user

        # if there has been one token in challenge mode, we only handle
        # challenges

        # if we got a validation against a sub_challenge, we extend this to
        # be a validation to all challenges of the transaction id
        import copy
        check_options = copy.deepcopy(options)
        state = check_options.get(
            'state', check_options.get('transactionid', ''))
        if state and '.' in state:
            transid = state.split('.')[0]
            if 'state' in check_options:
                check_options['state'] = transid
            if 'transactionid' in check_options:
                check_options['transactionid'] = transid

        # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

        # transaction id optimization - part 1:
        #
        # if we have a transaction id, we check only those tokens
        # that belong to this transaction id:

        challenges = []
        transaction_serials = []
        transid = check_options.get('state',
                                    check_options.get('transactionid', ''))
        if transid:
            expired, challenges = Challenges.get_challenges(transid=transid,
                                                            filter_open=True)
            for challenge in challenges:
                serial = challenge.tokenserial
                transaction_serials.append(serial)

        # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

        audit_entry = {}
        audit_entry['action_detail'] = "no token found!"

        challenge_tokens = []
        pin_matching_tokens = []
        invalid_tokens = []
        valid_tokens = []
        related_challenges = []

        # we have to preserve the result / reponse for token counters
        validation_results = {}

        for token in tokenList:

            # transaction id optimization - part 2:
            if transid:
                if token.getSerial() not in transaction_serials:
                    continue

            audit_entry['serial'] = token.getSerial()
            audit_entry['token_type'] = token.getType()

            # preselect: the token must be in the same realm as the user
            if user is not None:
                t_realms = token.token.getRealmNames()
                u_realm = user.realm
                if (len(t_realms) > 0 and len(u_realm) > 0 and
                        u_realm.lower() not in t_realms):

                    audit_entry['action_detail'] = ("Realm mismatch for "
                                                    "token and user")

                    continue

            # check if the token is the list of supported tokens
            # if not skip to the next token in list
            typ = token.getType()
            if typ.lower() not in tokenclass_registry:
                log.error('token typ %r not found in tokenclasses: %r' %
                          (typ, list(tokenclass_registry.keys())))
                audit_entry['action_detail'] = "Unknown Token type"
                continue

            if not token.isActive():
                audit_entry['action_detail'] = "Token inactive"
                continue

            if token.getFailCount() >= token.getMaxFailCount():
                audit_entry['action_detail'] = "Failcounter exceeded"
                token.incOtpFailCounter()
                continue

            # ---------------------------------------------------------------------- --

            # check for restricted path usage

            path = context['Path'].strip('/').partition('/')[0]
            token_path = token.getFromTokenInfo('scope', {}).get('path', [])

            if token_path and path not in token_path:
                continue

            # -------------------------------------------------------------- --

            # token validity handling

            now = datetime.now()

            if (token.validity_period_start and
                now < token.validity_period_start):

                audit_entry['action_detail'] = ("Authentication validity "
                                                "period mismatch!")

                token.incOtpFailCounter()

                continue

            token_success_excceed = (
                token.count_auth_success_max > 0 and
                token.count_auth_success >= token.count_auth_success_max)

            token_access_exceed = (
                token.count_auth_max > 0 and
                token.count_auth >= token.count_auth_max)

            token_expiry = (
                token.validity_period_end and
                now >= token.validity_period_end)

            if token_success_excceed or token_access_exceed or token_expiry:

                if token_access_exceed:
                    msg = "Authentication counter exceeded"

                if token_success_excceed:
                    msg = "Authentication sucess counter exceeded"

                if token_expiry:
                    msg = "Authentication validity period exceeded!"

                audit_entry['action_detail'] = msg

                token.incOtpFailCounter()

                # what should happen with exceeding tokens

                t_realms = None

                if not user.login and not user.realm:
                    t_realms = token.token.getRealmNames()

                if disable_on_authentication_exceed(user, realms=t_realms):
                    token.enable(False)

                if delete_on_authentication_exceed(user, realms=t_realms):
                    token.deleteToken()

                continue

            # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

            # start the token validation

            if not transid:
                # if there is no transaction id given we check all token
                # related challenges
                (_ex_challenges,
                 challenges) = Challenges.get_challenges(token,
                                                         options=check_options,
                                                         filter_open=True)

            try:
                (ret, reply) = token.check_token(
                    passw, user, options=check_options, challenges=challenges)
            except Exception as exx:
                # in case of a failure during checking token, we log the error
                # and continue with the next one
                log.exception("checking token %r failed: %r" % (token, exx))
                ret = -1
                reply = "%r" % exx
                audit_entry['action_detail'] = ("checking token %r "
                                                "failed: %r" % (token, exx))

                audit_entry['info'] = audit_entry.get('info','') + "%r" % exx

                continue
            finally:
                validation_results[token.getSerial()] = (ret, reply)

            (cToken, pToken, iToken, vToken) = token.get_verification_result()
            related_challenges.extend(token.related_challenges)

            challenge_tokens.extend(cToken)
            pin_matching_tokens.extend(pToken)
            invalid_tokens.extend(iToken)
            valid_tokens.extend(vToken)

        valid_tokens = list(set(valid_tokens))
        invalid_tokens = list(set(invalid_tokens))
        pin_matching_tokens = list(set(pin_matching_tokens))
        challenge_tokens = list(set(challenge_tokens))

        # end of token verification loop
        matching_challenges = []
        for token in valid_tokens:
            matching_challenges.extend(token.matching_challenges)

        matching_challenges = list(set(matching_challenges))

        # if there are related / sub challenges, we have to call their janitor
        Challenges.handle_related_challenge(matching_challenges)

        # now we finalize the token validation result
        fh = FinishTokens(valid_tokens,
                          challenge_tokens,
                          pin_matching_tokens,
                          invalid_tokens,
                          validation_results,
                          user, options,
                          audit_entry=audit_entry)

        (res, reply) = fh.finish_checked_tokens()

        # ------------------------------------------------------------------ --

        # add to all tokens the last accessed time stamp

        add_last_accessed_info(valid_tokens + pin_matching_tokens +
                               challenge_tokens + invalid_tokens)

        # add time stamp to all valid tokens

        add_last_verified_info(valid_tokens)

        # ------------------------------------------------------------------ --

        # now we care for all involved tokens and their challenges

        for token in (valid_tokens + pin_matching_tokens +
                      challenge_tokens + invalid_tokens):
            expired, _valid = Challenges.get_challenges(token)
            if expired:
                Challenges.delete_challenges(None, expired)

        log.debug("Number of valid tokens found "
                  "(validTokenNum): %d" % len(valid_tokens))

        return (res, reply)
Example #56
0
    def check_status(self, transid=None, user=None, serial=None,
                     password=None, use_offline=False):
        """
        check for open transactions - for polling support

        :param transid: the transaction id where we request the status from
        :param user: the token owner user
        :param serial: or the serial we are searching for
        :param password: the pin/password for authorization the request
        :param use_offline: on success the offline info is returned

        :return: tuple of success and detail dict
        """

        expired, challenges = Challenges.get_challenges(token=None, transid=transid)

        # remove all expired challenges
        if expired:
            Challenges.delete_challenges(None, expired)

        if not challenges:
            return False, None

        # there is only one challenge per transaction id
        # if not multiple challenges, where transaction id is the parent one
        reply = {}
        involved_tokens = []

        transactions = {}
        for ch in challenges:

            # only look for challenges that are not compromised
            if not Challenges.verify_checksum(ch):
                continue

            # is the requester authorized
            challenge_serial = ch.getTokenSerial()
            if serial and challenge_serial != serial:
                continue

            tokens = getTokens4UserOrSerial(serial=challenge_serial)
            if not tokens:
                continue

            involved_tokens.extend(tokens)

            # as one challenge belongs exactly to only one token,
            # we take this one as the token
            token = tokens[0]
            owner = get_token_owner(token)

            if user and user != owner:
                continue

            involved_tokens.extend(tokens)

            # we only check the user password / token pin if the user
            # paranmeter is given

            if user and owner:
                pin_match = check_pin(token, password, user=owner,
                                       options=None)
            else:
                pin_match = token.checkPin(password)

            if not pin_match:
                continue

            trans_dict = {}

            trans_dict['received_count'] = ch.received_count
            trans_dict['received_tan'] = ch.received_tan
            trans_dict['valid_tan'] = ch.valid_tan
            trans_dict['message'] = ch.challenge
            trans_dict['status'] = ch.getStatus()

            # -------------------------------------------------------------- --

            # extend the check status with the accept or deny of a transaction

            challenge_session = ch.getSession()

            if challenge_session:

                challenge_session_dict = json.loads(challenge_session)

                if 'accept' in challenge_session_dict:
                    trans_dict['accept'] = challenge_session_dict['accept']

                if 'reject' in challenge_session_dict:
                    trans_dict['reject'] = challenge_session_dict['reject']

            # -------------------------------------------------------------- --

            token_dict = {'serial': token.getSerial(), 'type': token.type}

            # 1. check if token supports offline at all
            supports_offline_at_all = token.supports_offline_mode

            # 2. check if policy allows to use offline authentication
            if user is not None and user.login and user.realm:
                realms = [user.realm]
            else:
                realms = token.getRealms()

            offline_is_allowed = supports_offline(realms, token)

            if not ch.is_open() and ch.valid_tan and \
               supports_offline_at_all and \
               offline_is_allowed and \
               use_offline:
                token_dict['offline_info'] = token.getOfflineInfo()

            trans_dict['token'] = token_dict
            transactions[ch.transid] = trans_dict

        if transactions:
            reply['transactions'] = transactions

        return len(reply) > 0, reply