Ejemplo n.º 1
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 = get_challenges(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
Ejemplo n.º 2
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 = get_challenges(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.error('[Ocra2TokenClass:statusValidationFail] Error during validation finalisation for token %r :%r' % (self.getSerial(), ex))
            log.error("[Ocra2TokenClass:statusValidationFail] %r" % (traceback.format_exc()))
            raise Exception(ex)

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

        log.debug('[statusValidationFail]')
        return
Ejemplo n.º 3
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 = get_challenges(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
Ejemplo n.º 4
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 = get_challenges(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 = get_challenges(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
Ejemplo n.º 5
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 = get_challenges(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.error('[Ocra2TokenClass:resync] unknown error: %r' % (ex))
            raise Exception('[Ocra2TokenClass:resync] unknown error: %s' % (ex))

        log.debug('[resync]: %r ' % (ret))
        return ret
Ejemplo n.º 6
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
        :type options:     dict

        :return:           verification counter or -1
        :rtype:            int (-1)
        """
        log.debug('%r: %r: %r', passw, counter, window)
        ret = -1

        challenges = []
        serial = self.getSerial()
        transid = options.get('transactionid', None)
        if transid is None:
            log.error("Could not checkOtp due to missing transaction id")
            raise Exception("Could not checkOtp due to missing transaction id")

        # get all challenges with a matching trasactionid
        challs = get_challenges(serial=serial, transid=transid)
        for chall in challs:
            (rec_tan, rec_valid) = chall.getTanStatus()
            if rec_tan is False:
                # add all untouched challenges
                challenges.append(chall)
            elif rec_valid is False:
                # don't allow touched but failed challenges
                pass

        if len(challenges) == 0:
            err = 'No open transaction found for token %s and transactionid %s' % (serial, transid)
            log.error(err)
            raise Exception(err)

        # decode the retrieved passw object
        try:
            authResponse = json.loads(passw)
        except ValueError as ex:
            log.exception("Invalid JSON format - value error %r", (ex))
            raise Exception("Invalid JSON format")

        try:
            signatureData = authResponse.get('signatureData', None)
            clientData = authResponse['clientData']
            keyHandle = authResponse['keyHandle']
        except AttributeError as ex:
            log.exception("Couldn't find keyword in JSON object - attribute error %r ", (ex))
            raise Exception("Couldn't find keyword in JSON object")

        # Does the keyHandle match the saved keyHandle created on registration?
        # Remove trailing '=' on the saved keyHandle
        savedKeyHandle = self.getFromTokenInfo('keyHandle', None)
        while savedKeyHandle.endswith('='):
            savedKeyHandle = savedKeyHandle[:-1]
        if keyHandle is None or keyHandle != savedKeyHandle:
            return -1

        # signatureData and clientData are urlsafe base64 encoded
        # correct padding errors (length should be multiples of 4)
        # fill up the signatureData and clientData with '=' to the correct padding
        signatureData = signatureData + ('=' * (4 - (len(signatureData) % 4)))
        clientData = clientData + ('=' * (4 - (len(clientData) % 4)))
        signatureData = base64.urlsafe_b64decode(signatureData.encode('ascii'))
        clientData = base64.urlsafe_b64decode(clientData.encode('ascii'))

        # now check the otp for each challenge
        for ch in challenges:
            challenge = {}

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

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

            # check the received clientData object and retrieve the appId
            cdOrigin = self._checkClientData(
                clientData, 'authentication', challenge['challenge'])

            # check the received origin
            if not self._is_valid_facet(cdOrigin):
                log.error("Received origin not in the valid facets. Aborting...")
                raise ValueError("Received origin not in the valid facets. Aborting...")

            # parse the received signatureData object
            (userPresenceByte, counter, signature) = self._parseSignatureData(signatureData)

            # the counter is interpreted as big-endian according to the U2F specification
            counterInt = struct.unpack('>I', counter)[0]

            # verify that the counter value increased - prevent token device cloning
            self._verifyCounterValue(counterInt)

            # prepare the applicationParameter and challengeParameter needed for
            # verification of the registration signature
            appId = cdOrigin
            if 'appid' in options:
                # The appid parameter is mandatory if a URL to the valid_facets action of
                # the u2f controller is sent to the token device. Since the token device
                # uses this given URL (and not the 'real' location origin as in the client
                # data object) for the applicationParameter, we have to adapt this behavior.
                appId = options.get('appid')
            applicationParameter = sha256(appId).digest()
            challengeParameter = sha256(clientData).digest()
            publicKey = base64.urlsafe_b64decode(
                self.getFromTokenInfo('publicKey', None).encode('ascii'))

            # verify the authentication signature
            self._validateAuthenticationSignature(applicationParameter,
                                                  userPresenceByte,
                                                  counter,
                                                  challengeParameter,
                                                  publicKey,
                                                  signature
                                                  )

            # U2F does not need an otp count
            ret = 0

        log.debug('%r', (ret))

        return ret