def create_challenge(self, transactionid=None, options=None): """ create a challenge, which is submitted to the user :param transactionid: the id of this challenge :param options: the request context parameters / data :return: tuple of (bool, message, transactionid, attributes) :rtype: tuple The return tuple builds up like this: ``bool`` if submit was successful; ``message`` which is displayed in the JSON response; additional ``attributes``, which are displayed in the JSON response. """ options = options or {} return_message = get_action_values_from_options( SCOPE.AUTH, "{0!s}_{1!s}".format(self.get_class_type(), ACTION.CHALLENGETEXT), options) or DEFAULT_CHALLENGE_TEXT position_count = int( get_action_values_from_options( SCOPE.AUTH, "{0!s}_{1!s}".format(self.get_class_type(), PIIXACTION.COUNT), options) or DEFAULT_POSITION_COUNT) attributes = {'state': transactionid} validity = 120 if self.is_active() is True: # We need to get a number of random positions from the secret string secret_length = len(self.token.get_otpkey().getKey()) if not secret_length: raise ValidateError( "The indexedsecret token has an empty secret and " "can not be used for authentication.") random_positions = [ urandom.randint(1, secret_length) for _x in range(0, position_count) ] position_str = ",".join( ["{0!s}".format(x) for x in random_positions]) attributes["random_positions"] = random_positions db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=options.get("challenge"), data=position_str, session=options.get("session"), validitytime=validity) db_challenge.save() transactionid = transactionid or db_challenge.transaction_id return_message = return_message.format(position_str) expiry_date = datetime.datetime.now() + \ datetime.timedelta(seconds=validity) attributes['valid_until'] = "{0!s}".format(expiry_date) return True, return_message, transactionid, attributes
def _build_smartphone_data(serial, challenge, fb_gateway, pem_privkey, options): """ Create the dictionary to be send to the smartphone as challenge :param challenge: base32 encoded random data string :type challenge: str :param fb_gateway: the gateway object containing the firebase configuration :type fb_gateway: privacyidea.lib.smsprovider.SMSProvider.ISMSProvider :param options: the options dictionary :type options: dict :return: the created smartphone_data dictionary :rtype: dict """ sslverify = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.SSL_VERIFY, options) or "1" sslverify = getParam({"sslverify": sslverify}, "sslverify", allowed_values=["0", "1"], default="1") # We send the challenge to the Firebase service url = fb_gateway.smsgateway.option_dict.get( FIREBASE_CONFIG.REGISTRATION_URL) message_on_mobile = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TEXT, options) or DEFAULT_MOBILE_TEXT title = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TITLE, options) or "privacyIDEA" smartphone_data = { "nonce": challenge, "question": message_on_mobile, "serial": serial, "title": title, "sslverify": sslverify, "url": url } # Create the signature. # value to string sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format( **smartphone_data) privkey_obj = serialization.load_pem_private_key(to_bytes(pem_privkey), None, default_backend()) # Sign the data with PKCS1 padding. Not all Androids support PSS padding. signature = privkey_obj.sign(sign_string.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) smartphone_data["signature"] = b32encode_and_unicode(signature) return smartphone_data
def has_further_challenge(self, options=None): """ Check if we should do multi challenge at all and then if there are further positions to query. :param options: Options dict :return: True, if further challenge is required. """ if self.get_tokeninfo("multichallenge"): transaction_id = options.get('transaction_id') challengeobject_list = get_challenges( serial=self.token.serial, transaction_id=transaction_id) position_count = int( get_action_values_from_options( SCOPE.AUTH, "{0!s}_{1!s}".format( self.get_class_type(), PIIXACTION.COUNT), options) or DEFAULT_POSITION_COUNT) if len(challengeobject_list) == 1: session = int(challengeobject_list[0].session or "0") + 1 options["session"] = u"{0!s}".format(session) if session < position_count: return True return False
def create_challenge(self, transactionid=None, options=None): """ create a challenge, which is submitted to the user :param transactionid: the id of this challenge :param options: the request context parameters / data You can pass exception=1 to raise an exception, if the SMS could not be sent. Otherwise the message is contained in the response. :return: tuple of (bool, message and data) bool, if submit was successful message is submitted to the user data is preserved in the challenge attributes - additional attributes, which are displayed in the output """ success = False options = options or {} return_message = get_action_values_from_options( SCOPE.AUTH, "{0!s}_{1!s}".format(self.get_class_type(), ACTION.CHALLENGETEXT), options) or _("Enter the OTP from the SMS:") attributes = {'state': transactionid} validity = self._get_sms_timeout() if self.is_active() is True: counter = self.get_otp_count() log.debug("counter={0!r}".format(counter)) self.inc_otp_counter(counter, reset=False) # At this point we must not bail out in case of an # Gateway error, since checkPIN is successful. A bail # out would cancel the checking of the other tokens try: message_template = self._get_sms_text(options) success, sent_message = self._send_sms( message=message_template) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=options.get("challenge"), session=options.get("session"), validitytime=validity) db_challenge.save() transactionid = transactionid or db_challenge.transaction_id except Exception as e: info = ("The PIN was correct, but the " "SMS could not be sent: %r" % e) log.warning(info) log.debug("{0!s}".format(traceback.format_exc())) return_message = info if is_true(options.get("exception")): raise Exception(info) expiry_date = datetime.datetime.now() + \ datetime.timedelta(seconds=validity) attributes['valid_until'] = "{0!s}".format(expiry_date) return success, return_message, transactionid, attributes
def create_challenge(self, transactionid=None, options=None): """ create a challenge, which is submitted to the user :param transactionid: the id of this challenge :param options: the request context parameters / data You can pass exception=1 to raise an exception, if the SMS could not be sent. Otherwise the message is contained in the response. :return: tuple of (bool, message and data) bool, if submit was successful message is submitted to the user data is preserved in the challenge attributes - additional attributes, which are displayed in the output """ success = False options = options or {} return_message = get_action_values_from_options(SCOPE.AUTH, "{0!s}_{1!s}".format(self.get_class_type(), ACTION.CHALLENGETEXT), options) or _("Enter the OTP from the SMS:") attributes = {'state': transactionid} validity = self._get_sms_timeout() if self.is_active() is True: counter = self.get_otp_count() log.debug("counter={0!r}".format(counter)) self.inc_otp_counter(counter, reset=False) # At this point we must not bail out in case of an # Gateway error, since checkPIN is successful. A bail # out would cancel the checking of the other tokens try: message_template = self._get_sms_text(options) success, sent_message = self._send_sms( message=message_template) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=options.get("challenge"), session=options.get("session"), validitytime=validity) db_challenge.save() transactionid = transactionid or db_challenge.transaction_id except Exception as e: info = ("The PIN was correct, but the " "SMS could not be sent: %r" % e) log.warning(info) log.debug("{0!s}".format(traceback.format_exc())) return_message = info if is_true(options.get("exception")): raise Exception(info) expiry_date = datetime.datetime.now() + \ datetime.timedelta(seconds=validity) attributes['valid_until'] = "{0!s}".format(expiry_date) return success, return_message, transactionid, attributes
def _build_smartphone_data(serial, challenge, registration_url, pem_privkey, options): """ Create the dictionary to be send to the smartphone as challenge :param challenge: base32 encoded random data string :type challenge: str :param registration_url: The privacyIDEA URL, to which the Push token communicates :type registration_url: str :param options: the options dictionary :type options: dict :return: the created smartphone_data dictionary :rtype: dict """ sslverify = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.SSL_VERIFY, options) or "1" sslverify = getParam({"sslverify": sslverify}, "sslverify", allowed_values=["0", "1"], default="1") message_on_mobile = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TEXT, options) or DEFAULT_MOBILE_TEXT title = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TITLE, options) or "privacyIDEA" smartphone_data = { "nonce": challenge, "question": message_on_mobile, "serial": serial, "title": title, "sslverify": sslverify, "url": registration_url } # Create the signature. # value to string sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format( **smartphone_data) privkey_obj = serialization.load_pem_private_key(to_bytes(pem_privkey), None, default_backend()) # Sign the data with PKCS1 padding. Not all Androids support PSS padding. signature = privkey_obj.sign(sign_string.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) smartphone_data["signature"] = b32encode_and_unicode(signature) return smartphone_data
def create_challenge(self, transactionid=None, options=None): """ This method creates a challenge, which is submitted to the user. The submitted challenge will be preserved in the challenge database. If no transaction id is given, the system will create a transaction id and return it, so that the response can refer to this transaction. :param transactionid: the id of this challenge :param options: the request context parameters / data :type options: dict :return: tuple of (bool, message, transactionid, attributes) :rtype: tuple The return tuple builds up like this: ``bool`` if submit was successful; ``message`` which is displayed in the JSON response; additional ``attributes``, which are displayed in the JSON response. """ options = options or {} message = get_action_values_from_options(SCOPE.AUTH, "{0!s}_{1!s}".format(self.get_class_type(), ACTION.CHALLENGETEXT), options)or _(u'Please confirm with your U2F token ({0!s})').format( self.token.description) validity = int(get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) challenge = geturandom(32) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=hexlify_and_unicode(challenge), data=None, session=options.get("session"), validitytime=validity) db_challenge.save() sec_object = self.token.get_otpkey() key_handle_hex = sec_object.getKey() key_handle_bin = binascii.unhexlify(key_handle_hex) key_handle_url = url_encode(key_handle_bin) challenge_url = url_encode(challenge) u2f_sign_request = {"appId": self.get_tokeninfo("appId"), "version": U2F_Version, "challenge": challenge_url, "keyHandle": key_handle_url} image_url = IMAGES.get(self.token.description.lower().split()[0], "") response_details = {"u2fSignRequest": u2f_sign_request, "hideResponseInput": True, "img": image_url} return True, message, db_challenge.transaction_id, response_details
def create_challenge(self, transactionid=None, options=None): """ This method creates a challenge, which is submitted to the user. The submitted challenge will be preserved in the challenge database. If no transaction id is given, the system will create a transaction id and return it, so that the response can refer to this transaction. :param transactionid: the id of this challenge :param options: the request context parameters / data :type options: dict :return: tuple of (bool, message, transactionid, attributes) :rtype: tuple The return tuple builds up like this: ``bool`` if submit was successful; ``message`` which is displayed in the JSON response; additional ``attributes``, which are displayed in the JSON response. """ options = options or {} message = get_action_values_from_options(SCOPE.AUTH, "{0!s}_{1!s}".format(self.get_class_type(), ACTION.CHALLENGETEXT), options)or _(u'Please confirm with your U2F token ({0!s})').format( self.token.description) validity = int(get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) challenge = geturandom(32) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=binascii.hexlify(challenge), data=None, session=options.get("session"), validitytime=validity) db_challenge.save() sec_object = self.token.get_otpkey() key_handle_hex = sec_object.getKey() key_handle_bin = binascii.unhexlify(key_handle_hex) key_handle_url = url_encode(key_handle_bin) challenge_url = url_encode(challenge) u2f_sign_request = {"appId": self.get_tokeninfo("appId"), "version": U2F_Version, "challenge": challenge_url, "keyHandle": key_handle_url} image_url = IMAGES.get(self.token.description.lower().split()[0], "") response_details = {"u2fSignRequest": u2f_sign_request, "hideResponseInput": True, "img": image_url} return True, message, db_challenge.transaction_id, response_details
def test_24_challenge_text(self): g = FakeFlaskG() g.client_ip = "10.0.0.1" options = {"g": g, "user": User("cornelius", self.realm1)} set_policy("chaltext", scope=SCOPE.AUTH, action="{0!s}=Wo du wolle?".format(ACTION.CHALLENGETEXT)) g.policy_object = PolicyClass() val = get_action_values_from_options(SCOPE.AUTH, ACTION.CHALLENGETEXT, options) self.assertEqual(val, u"Wo du wolle?") delete_policy("chaltext")
def has_further_challenge(self, options=None): """ Check if there are still more questions to be asked. :param options: Options dict :return: True, if further challenge is required. """ transaction_id = options.get('transaction_id') challengeobject_list = get_challenges(serial=self.token.serial, transaction_id=transaction_id) question_number = int(get_action_values_from_options(SCOPE.AUTH, "{0!s}_{1!s}".format(self.get_class_type(), QUESTACTION.NUM_QUESTIONS), options) or 1) if len(challengeobject_list) == 1: session = int(challengeobject_list[0].session or "0") + 1 options["session"] = u"{0!s}".format(session) # write the used questions to the data field options["data"] = challengeobject_list[0].data or "" if session < question_number: return True return False
def create_challenge(self, transactionid=None, options=None): """ create a challenge, which is submitted to the user :param transactionid: the id of this challenge :param options: the request context parameters / data You can pass ``exception=1`` to raise an exception, if the SMS could not be sent. Otherwise the message is contained in the response. :return: tuple of (success, message, transactionid, attributes) * success: if submit was successful * message: the text submitted to the user * transactionid: the given or generated transactionid * reply_dict: additional dictionary, which is added to the response :rtype: tuple(bool, str, str, dict) """ success = False options = options or {} return_message = get_action_values_from_options( SCOPE.AUTH, "{0!s}_{1!s}".format(self.get_class_type(), ACTION.CHALLENGETEXT), options) or _("Enter the OTP from the Email:") reply_dict = {'attributes': {'state': transactionid}} validity = int(get_from_config("email.validtime", 120)) if self.is_active() is True: counter = self.get_otp_count() log.debug("counter={0!r}".format(counter)) self.inc_otp_counter(counter, reset=False) # At this point we must not bail out in case of an # Gateway error, since checkPIN is successful. A bail # out would cancel the checking of the other tokens try: message_template, mimetype = self._get_email_text_or_subject( options) subject_template, _n = self._get_email_text_or_subject( options, EMAILACTION.EMAILSUBJECT, "Your OTP") # Create the challenge in the database if is_true(get_from_config("email.concurrent_challenges")): data = self.get_otp()[2] else: data = None db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=options.get("challenge"), data=data, session=options.get("session"), validitytime=validity) db_challenge.save() transactionid = transactionid or db_challenge.transaction_id # We send the email after creating the challenge for testing. success, sent_message = self._compose_email( message=message_template, subject=subject_template, mimetype=mimetype) except Exception as e: info = ("The PIN was correct, but the " "EMail could not be sent: %r" % e) log.warning(info) log.debug(u"{0!s}".format(traceback.format_exc())) return_message = info if is_true(options.get("exception")): raise Exception(info) expiry_date = datetime.datetime.now() + \ datetime.timedelta(seconds=validity) reply_dict['attributes']['valid_until'] = "{0!s}".format(expiry_date) return success, return_message, transactionid, reply_dict
def create_challenge(self, transactionid=None, options=None): """ This method creates a challenge, which is submitted to the user. The submitted challenge will be preserved in the challenge database. If no transaction id is given, the system will create a transaction id and return it, so that the response can refer to this transaction. :param transactionid: the id of this challenge :param options: the request context parameters / data :type options: dict :return: tuple of (bool, message, transactionid, attributes) :rtype: tuple The return tuple builds up like this: ``bool`` if submit was successful; ``message`` which is displayed in the JSON response; additional challenge ``reply_dict``, which are displayed in the JSON challenges response. """ options = options or {} message = get_action_values_from_options( SCOPE.AUTH, ACTION.CHALLENGETEXT, options) or DEFAULT_CHALLENGE_TEXT reply_dict = {} data = None # Initially we assume there is no error from Firebase res = True fb_identifier = self.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG) if fb_identifier: challenge = b32encode_and_unicode(geturandom()) if fb_identifier != POLL_ONLY: # We only push to Firebase if this tokens does NOT POLL_ONLY. fb_gateway = create_sms_instance(fb_identifier) registration_url = get_action_values_from_options( SCOPE.ENROLL, PUSH_ACTION.REGISTRATION_URL, options=options) pem_privkey = self.get_tokeninfo(PRIVATE_KEY_SERVER) smartphone_data = _build_smartphone_data( self.token.serial, challenge, registration_url, pem_privkey, options) res = fb_gateway.submit_message( self.get_tokeninfo("firebase_token"), smartphone_data) # Create the challenge in the challenge table if either the message # was successfully submitted to the Firebase API or if polling is # allowed in general or for this specific token. allow_polling = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.ALLOW_POLLING, options=options) or PushAllowPolling.ALLOW if ((allow_polling == PushAllowPolling.ALLOW or (allow_polling == PushAllowPolling.TOKEN and is_true( self.get_tokeninfo(POLLING_ALLOWED, default='True')))) or res): validity = int( get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() # Maybe there is a PushChallengeValidityTime... lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=challenge, data=data, session=options.get("session"), validitytime=validity) db_challenge.save() self.challenge_janitor() # If sending the Push message failed, we still raise an error and a warning. if not res: log.warning( u"Failed to submit message to Firebase service for token {0!s}." .format(self.token.serial)) raise ValidateError( "Failed to submit message to Firebase service.") else: log.warning(u"The token {0!s} has no tokeninfo {1!s}. " u"The message could not be sent.".format( self.token.serial, PUSH_ACTION.FIREBASE_CONFIG)) raise ValidateError( "The token has no tokeninfo. Can not send via Firebase service." ) return True, message, db_challenge.transaction_id, reply_dict
def _api_endpoint_get(cls, g, request_data): """ Handle all GET requests to the api endpoint. Currently this is only used for polling. :param g: The Flask context :param request_data: Dictionary containing the parameters of the request :type request_data: dict :returns: Result of the polling operation, 'True' if an unanswered and matching challenge exists, 'False' otherwise. :rtype: bool """ # By default we allow polling if the policy is not set. allow_polling = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.ALLOW_POLLING, options={'g': g}) or PushAllowPolling.ALLOW if allow_polling == PushAllowPolling.DENY: raise PolicyError('Polling not allowed!') serial = getParam(request_data, "serial", optional=False) timestamp = getParam(request_data, 'timestamp', optional=False) signature = getParam(request_data, 'signature', optional=False) # first check if the timestamp is in the required span cls._check_timestamp_in_range(timestamp, POLL_TIME_WINDOW) # now check the signature # first get the token try: tok = get_one_token(serial=serial, tokentype=cls.get_class_type()) # If the push_allow_polling policy is set to "token" we also # need to check the POLLING_ALLOWED tokeninfo. If it evaluated # to 'False', polling is not allowed for this token. If the # tokeninfo value evaluates to 'True' or is not set at all, # polling is allowed for this token. if allow_polling == PushAllowPolling.TOKEN: if not is_true( tok.get_tokeninfo(POLLING_ALLOWED, default='True')): log.debug('Polling not allowed for pushtoken {0!s} due to ' 'tokeninfo.'.format(serial)) raise PolicyError('Polling not allowed!') pubkey_obj = _build_verify_object( tok.get_tokeninfo(PUBLIC_KEY_SMARTPHONE)) sign_data = u"{serial}|{timestamp}".format(**request_data) pubkey_obj.verify(b32decode(signature), sign_data.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) # The signature was valid now check for an open challenge # we need the private server key to sign the smartphone data pem_privkey = tok.get_tokeninfo(PRIVATE_KEY_SERVER) # We need the registration URL for the challenge registration_url = get_action_values_from_options( SCOPE.ENROLL, PUSH_ACTION.REGISTRATION_URL, options={'g': g}) if not registration_url: raise ResourceNotFoundError( 'There is no registration_url defined for the ' ' pushtoken {0!s}. You need to define a push_registration_url ' 'in an enrollment policy.'.format(serial)) options = {'g': g} challenges = [] challengeobject_list = get_challenges(serial=serial) for chal in challengeobject_list: # check if the challenge is active and not already answered _cnt, answered = chal.get_otp_status() if not answered and chal.is_valid(): # then return the necessary smartphone data to answer # the challenge sp_data = _build_smartphone_data(serial, chal.challenge, registration_url, pem_privkey, options) challenges.append(sp_data) # return the challenges as a list in the result value result = challenges except (ResourceNotFoundError, ParameterError, InvalidSignature, ConfigAdminError, BinasciiError) as e: # to avoid disclosing information we always fail with an invalid # signature error even if the token with the serial could not be found log.debug('{0!s}'.format(traceback.format_exc())) log.info('The following error occurred during the signature ' 'check: "{0!r}"'.format(e)) raise privacyIDEAError('Could not verify signature!') return result
def create_challenge(self, transactionid=None, options=None): """ create a challenge, which is submitted to the user :param transactionid: the id of this challenge :param options: the request context parameters / data :return: tuple of (bool, message and data) bool, if submit was successful message is submitted to the user data is preserved in the challenge attributes - additional attributes, which are displayed in the output """ success = False options = options or {} return_message = get_action_values_from_options( SCOPE.AUTH, "{0!s}_{1!s}".format(self.get_class_type(), ACTION.CHALLENGETEXT), options) or _("Enter the OTP from the Email:") attributes = {'state': transactionid} validity = int(get_from_config("email.validtime", 120)) if self.is_active() is True: counter = self.get_otp_count() log.debug("counter={0!r}".format(counter)) self.inc_otp_counter(counter, reset=False) # At this point we must not bail out in case of an # Gateway error, since checkPIN is successful. A bail # out would cancel the checking of the other tokens try: message_template, mimetype = self._get_email_text_or_subject( options) subject_template, _n = self._get_email_text_or_subject( options, EMAILACTION.EMAILSUBJECT, "Your OTP") # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=options.get("challenge"), session=options.get("session"), validitytime=validity) db_challenge.save() transactionid = transactionid or db_challenge.transaction_id # We send the email after creating the challenge for testing. success, sent_message = self._compose_email( message=message_template, subject=subject_template, mimetype=mimetype) except Exception as e: info = ("The PIN was correct, but the " "EMail could not be sent: %r" % e) log.warning(info) log.debug(u"{0!s}".format(traceback.format_exc(e))) return_message = info expiry_date = datetime.datetime.now() + \ datetime.timedelta(seconds=validity) attributes['valid_until'] = "{0!s}".format(expiry_date) return success, return_message, transactionid, attributes
def create_challenge(self, transactionid=None, options=None): """ This method creates a challenge, which is submitted to the user. The submitted challenge will be preserved in the challenge database. If no transaction id is given, the system will create a transaction id and return it, so that the response can refer to this transaction. :param transactionid: the id of this challenge :param options: the request context parameters / data :type options: dict :return: tuple of (bool, message, transactionid, attributes) :rtype: tuple The return tuple builds up like this: ``bool`` if submit was successful; ``message`` which is displayed in the JSON response; additional ``attributes``, which are displayed in the JSON response. """ options = options or {} message = get_action_values_from_options( SCOPE.AUTH, ACTION.CHALLENGETEXT, options) or DEFAULT_CHALLENGE_TEXT attributes = None data = None fb_identifier = self.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG) if fb_identifier: challenge = b32encode_and_unicode(geturandom()) fb_gateway = create_sms_instance(fb_identifier) pem_privkey = self.get_tokeninfo(PRIVATE_KEY_SERVER) smartphone_data = _build_smartphone_data(self.token.serial, challenge, fb_gateway, pem_privkey, options) res = fb_gateway.submit_message( self.get_tokeninfo("firebase_token"), smartphone_data) if not res: raise ValidateError( "Failed to submit message to firebase service.") else: log.warning(u"The token {0!s} has no tokeninfo {1!s}. " u"The message could not be sent.".format( self.token.serial, PUSH_ACTION.FIREBASE_CONFIG)) raise ValidateError( "The token has no tokeninfo. Can not send via firebase service." ) validity = int(get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() # Maybe there is a PushChallengeValidityTime... lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=challenge, data=data, session=options.get("session"), validitytime=validity) db_challenge.save() self.challenge_janitor() return True, message, db_challenge.transaction_id, attributes
def api_endpoint(cls, request, g): """ This provides a function which is called by the API endpoint ``/ttype/push`` which is defined in :doc:`../../api/ttype` The method returns a tuple ``("json", {})`` This endpoint provides several functionalities: - It is used for the 2nd enrollment step of the smartphone. It accepts the following parameters: .. sourcecode:: http POST /ttype/push HTTP/1.1 Host: https://yourprivacyideaserver serial=<token serial> fbtoken=<firebase token> pubkey=<public key> - It is also used when the smartphone sends the signed response to the challenge during authentication. The following parameters ar accepted: .. sourcecode:: http POST /ttype/push HTTP/1.1 Host: https://yourprivacyideaserver serial=<token serial> nonce=<the actual challenge> signature=<the signed nonce> - And it also acts as an endpoint for polling challenges: .. sourcecode:: http GET /ttype/push HTTP/1.1 Host: https://yourprivacyideaserver serial=<tokenserial> timestamp=<timestamp> signature=SIGNATURE(<tokenserial>|<timestamp>) More on polling can be found here: https://github.com/privacyidea/privacyidea/wiki/concept%3A-pushtoken-poll :param request: The Flask request :param g: The Flask global object g :return: The json string representing the result dictionary :rtype: tuple("json", str) """ details = {} result = False if request.method == 'POST': serial = getParam(request.all_data, "serial", optional=False) if serial and "fbtoken" in request.all_data and "pubkey" in request.all_data: log.debug("Do the 2nd step of the enrollment.") try: token_obj = get_one_token(serial=serial, tokentype="push", rollout_state="clientwait") token_obj.update(request.all_data) except ResourceNotFoundError: raise ResourceNotFoundError( "No token with this serial number " "in the rollout state 'clientwait'.") init_detail_dict = request.all_data details = token_obj.get_init_detail(init_detail_dict) result = True elif serial and "nonce" in request.all_data and "signature" in request.all_data: log.debug( "Handling the authentication response from the smartphone." ) challenge = getParam(request.all_data, "nonce") serial = getParam(request.all_data, "serial") signature = getParam(request.all_data, "signature") # get the token_obj for the given serial: token_obj = get_one_token(serial=serial, tokentype="push") pubkey_obj = _build_verify_object( token_obj.get_tokeninfo(PUBLIC_KEY_SMARTPHONE)) # Do the 2nd step of the authentication # Find valid challenges challengeobject_list = get_challenges(serial=serial, challenge=challenge) if challengeobject_list: # There are valid challenges, so we check this signature for chal in challengeobject_list: # verify the signature of the nonce sign_data = u"{0!s}|{1!s}".format(challenge, serial) try: pubkey_obj.verify(b32decode(signature), sign_data.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) # The signature was valid log.debug( "Found matching challenge {0!s}.".format(chal)) chal.set_otp_status(True) chal.save() result = True except InvalidSignature as _e: pass else: raise ParameterError("Missing parameters!") elif request.method == 'GET': # This is only used for polling # By default we allow polling if the policy is not set. allow_polling = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.ALLOW_POLLING, options={'g': g}) or PushAllowPolling.ALLOW if allow_polling == PushAllowPolling.DENY: raise PolicyError('Polling not allowed!') serial = getParam(request.all_data, "serial", optional=False) timestamp = getParam(request.all_data, 'timestamp', optional=False) signature = getParam(request.all_data, 'signature', optional=False) # first check if the timestamp is in the required span try: ts = isoparse(timestamp) except (ValueError, TypeError) as _e: log.debug('{0!s}'.format(traceback.format_exc())) raise privacyIDEAError( 'Could not parse timestamp {0!s}. ' 'ISO-Format required.'.format(timestamp)) # TODO: make time delta configurable td = timedelta(minutes=POLL_TIME_WINDOW) # We don't know if the passed timestamp is timezone aware. If no # timezone is passed, we assume UTC if ts.tzinfo: now = datetime.now(utc) else: now = datetime.utcnow() if not (now - td <= ts <= now + td): raise privacyIDEAError( 'Timestamp {0!s} not in valid range.'.format(timestamp)) # now check the signature # first get the token try: tok = get_one_token(serial=serial, tokentype=cls.get_class_type()) # If the push_allow_polling policy is set to "token" we also # need to check the POLLING_ALLOWED tokeninfo. If it evaluated # to 'False', polling is not allowed for this token. If the # tokeninfo value evaluates to 'True' or is not set at all, # polling is allowed for this token. if allow_polling == PushAllowPolling.TOKEN: if not is_true( tok.get_tokeninfo(POLLING_ALLOWED, default='True')): log.debug( 'Polling not allowed for pushtoken {0!s} due to ' 'tokeninfo.'.format(serial)) raise PolicyError('Polling not allowed!') pubkey_obj = _build_verify_object( tok.get_tokeninfo(PUBLIC_KEY_SMARTPHONE)) sign_data = u"{serial}|{timestamp}".format(**request.all_data) pubkey_obj.verify(b32decode(signature), sign_data.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) # The signature was valid now check for an open challenge # we need the private server key to sign the smartphone data pem_privkey = tok.get_tokeninfo(PRIVATE_KEY_SERVER) # we also need the FirebaseGateway for this token fb_identifier = tok.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG) if not fb_identifier: raise ResourceNotFoundError( 'The pushtoken {0!s} has no Firebase configuration ' 'assigned.'.format(serial)) fb_gateway = create_sms_instance(fb_identifier) options = {'g': g} challenges = [] challengeobject_list = get_challenges(serial=serial) for chal in challengeobject_list: # check if the challenge is active and not already answered _cnt, answered = chal.get_otp_status() if not answered and chal.is_valid(): # then return the necessary smartphone data to answer # the challenge sp_data = _build_smartphone_data( serial, chal.challenge, fb_gateway, pem_privkey, options) challenges.append(sp_data) # return the challenges as a list in the result value result = challenges except (ResourceNotFoundError, ParameterError, InvalidSignature, ConfigAdminError, BinasciiError) as e: # to avoid disclosing information we always fail with an invalid # signature error even if the token with the serial could not be found log.debug('{0!s}'.format(traceback.format_exc())) log.info('The following error occurred during the signature ' 'check: "{0!r}"'.format(e)) raise privacyIDEAError('Could not verify signature!') else: raise privacyIDEAError( 'Method {0!s} not allowed in \'api_endpoint\' ' 'for push token.'.format(request.method)) return "json", prepare_result(result, details=details)
def create_challenge(self, transactionid=None, options=None): """ This method creates a challenge, which is submitted to the user. The submitted challenge will be preserved in the challenge database. If no transaction id is given, the system will create a transaction id and return it, so that the response can refer to this transaction. :param transactionid: the id of this challenge :param options: the request context parameters / data :type options: dict :return: tuple of (bool, message, transactionid, attributes) :rtype: tuple The return tuple builds up like this: ``bool`` if submit was successful; ``message`` which is displayed in the JSON response; additional ``attributes``, which are displayed in the JSON response. """ res = False options = options or {} message = get_action_values_from_options(SCOPE.AUTH, ACTION.CHALLENGETEXT, options) or DEFAULT_CHALLENGE_TEXT sslverify = get_action_values_from_options(SCOPE.AUTH, PUSH_ACTION.SSL_VERIFY, options) or "1" sslverify = getParam({"sslverify": sslverify}, "sslverify", allowed_values=["0", "1"], default="1") attributes = None data = None challenge = b32encode_and_unicode(geturandom()) fb_identifier = self.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG) if fb_identifier: # We send the challenge to the Firebase service fb_gateway = create_sms_instance(fb_identifier) url = fb_gateway.smsgateway.option_dict.get(FIREBASE_CONFIG.REGISTRATION_URL) message_on_mobile = get_action_values_from_options(SCOPE.AUTH, PUSH_ACTION.MOBILE_TEXT, options) or DEFAULT_MOBILE_TEXT title = get_action_values_from_options(SCOPE.AUTH, PUSH_ACTION.MOBILE_TITLE, options) or "privacyIDEA" smartphone_data = {"nonce": challenge, "question": message_on_mobile, "serial": self.token.serial, "title": title, "sslverify": sslverify, "url": url} # Create the signature. # value to string sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format(**smartphone_data) pem_privkey = self.get_tokeninfo(PRIVATE_KEY_SERVER) privkey_obj = serialization.load_pem_private_key(to_bytes(pem_privkey), None, default_backend()) # Sign the data with PKCS1 padding. Not all Androids support PSS padding. signature = privkey_obj.sign(sign_string.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) smartphone_data["signature"] = b32encode_and_unicode(signature) res = fb_gateway.submit_message(self.get_tokeninfo("firebase_token"), smartphone_data) if not res: raise ValidateError("Failed to submit message to firebase service.") else: log.warning(u"The token {0!s} has no tokeninfo {1!s}. " u"The message could not be sent.".format(self.token.serial, PUSH_ACTION.FIREBASE_CONFIG)) raise ValidateError("The token has no tokeninfo. Can not send via firebase service.") validity = int(get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() # Maybe there is a PushChallengeValidityTime... lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=challenge, data=data, session=options.get("session"), validitytime=validity) db_challenge.save() self.challenge_janitor() return True, message, db_challenge.transaction_id, attributes
def create_challenge(self, transactionid=None, options=None): """ This method creates a challenge, which is submitted to the user. The submitted challenge will be preserved in the challenge database. If no transaction id is given, the system will create a transaction id and return it, so that the response can refer to this transaction. :param transactionid: the id of this challenge :param options: the request context parameters / data :type options: dict :return: tuple of (bool, message, transactionid, attributes) :rtype: tuple The return tuple builds up like this: ``bool`` if submit was successful; ``message`` which is displayed in the JSON response; additional ``attributes``, which are displayed in the JSON response. """ res = False options = options or {} message = get_action_values_from_options( SCOPE.AUTH, ACTION.CHALLENGETEXT, options) or DEFAULT_CHALLENGE_TEXT sslverify = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.SSL_VERIFY, options) or "1" sslverify = getParam({"sslverify": sslverify}, "sslverify", allowed_values=["0", "1"], default="1") attributes = None data = None challenge = b32encode_and_unicode(geturandom()) fb_identifier = self.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG) if fb_identifier: # We send the challenge to the Firebase service fb_gateway = create_sms_instance(fb_identifier) url = fb_gateway.smsgateway.option_dict.get( FIREBASE_CONFIG.REGISTRATION_URL) message_on_mobile = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TEXT, options) or DEFAULT_MOBILE_TEXT title = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TITLE, options) or "privacyIDEA" smartphone_data = { "nonce": challenge, "question": message_on_mobile, "serial": self.token.serial, "title": title, "sslverify": sslverify, "url": url } # Create the signature. # value to string sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format( **smartphone_data) pem_privkey = self.get_tokeninfo(PRIVATE_KEY_SERVER) privkey_obj = serialization.load_pem_private_key( to_bytes(pem_privkey), None, default_backend()) # Sign the data with PKCS1 padding. Not all Androids support PSS padding. signature = privkey_obj.sign(sign_string.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) smartphone_data["signature"] = b32encode_and_unicode(signature) res = fb_gateway.submit_message( self.get_tokeninfo("firebase_token"), smartphone_data) if not res: raise ValidateError( "Failed to submit message to firebase service.") else: log.warning(u"The token {0!s} has no tokeninfo {1!s}. " u"The message could not be sent.".format( self.token.serial, PUSH_ACTION.FIREBASE_CONFIG)) raise ValidateError( "The token has no tokeninfo. Can not send via firebase service." ) validity = int(get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() # Maybe there is a PushChallengeValidityTime... lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=challenge, data=data, session=options.get("session"), validitytime=validity) db_challenge.save() self.challenge_janitor() return True, message, db_challenge.transaction_id, attributes