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
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
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
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
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
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