def setUp(self): # Use a the gateway definition for configuring the provider identifier = "mySmppGW" id = set_smsgateway(identifier, self.provider_module, description="test", options=self.config) self.assertTrue(id > 0) self.provider = create_sms_instance(identifier=identifier) self.assertEqual(type(self.provider), SmppSMSProvider)
def test_00_errors(self): # No smsgateway defined s = SmppSMSProvider() self.assertRaises(SMSError, s.submit_message, "phone", "message") # No host defined set_smsgateway("missing_host", self.provider_module, options={"SMSC_PORT": "1234"}) p = create_sms_instance(identifier="missing_host") self.assertRaises(SMSError, p.submit_message, "phone", "message") delete_smsgateway("missing_host") # No port defined set_smsgateway("missing_port", self.provider_module, options={"SMSC_HOST": "1.1.1.1"}) p = create_sms_instance(identifier="missing_port") self.assertRaises(SMSError, p.submit_message, "phone", "message") delete_smsgateway("missing_port")
def _send_sms(self, message="<otp>"): """ send sms :param message: the sms submit message - could contain placeholders like <otp> or <serial> :type message: string :return: submitted message :rtype: string """ ret = None phone = self.get_tokeninfo("phone") otp = self.get_otp()[2] serial = self.get_serial() message = message.replace("<otp>", otp) message = message.replace("<serial>", serial) log.debug("sending SMS to phone number {0!s} ".format(phone)) # First we try to get the new SMS gateway config style sms_gateway_identifier = get_from_config("sms.identifier") if sms_gateway_identifier: # New style sms = create_sms_instance(sms_gateway_identifier) else: # Old style (SMSProvider, SMSProviderClass) = self._get_sms_provider() log.debug("smsprovider: {0!s}, class: {1!s}".format(SMSProvider, SMSProviderClass)) try: sms = get_sms_provider_class(SMSProvider, SMSProviderClass)() except Exception as exc: log.error("Failed to load SMSProvider: {0!r}".format(exc)) log.debug("{0!s}".format(traceback.format_exc())) raise exc try: # now we need the config from the env log.debug("loading SMS configuration for class {0!s}".format(sms)) config = self._get_sms_provider_config() log.debug("config: {0!r}".format(config)) sms.load_config(config) except Exception as exc: log.error("Failed to load sms.providerConfig: {0!r}".format(exc)) log.debug("{0!s}".format(traceback.format_exc())) raise Exception("Failed to load sms.providerConfig: {0!r}".format(exc)) log.debug("submitMessage: {0!r}, to phone {1!r}".format(message, phone)) ret = sms.submit_message(phone, message) return ret, message
def test_08_smsgateway_success(self): responses.add(responses.POST, self.url) identifier = "mySMS" provider_module = "privacyidea.lib.smsprovider.SipgateSMSProvider" \ ".SipgateSMSProvider" id = set_smsgateway(identifier, provider_module, description="test", options=self.config) self.assertTrue(id > 0) sms = create_sms_instance(identifier) r = sms.submit_message("123456", "Hello") self.assertTrue(r)
def test_03_create_instance_by_identifier(self): # SMS gateway definition identifier = "myGW" provider_module = "privacyidea.lib.smsprovider.HttpSMSProvider" \ ".HttpSMSProvider" id = set_smsgateway(identifier, provider_module, description="test", options={"HTTP_METHOD": "POST", "URL": "example.com"}) self.assertTrue(id > 0) sms = create_sms_instance(identifier) self.assertEqual(sms.smsgateway.option_dict.get("URL"), "example.com") self.assertEqual(sms.smsgateway.option_dict.get("HTTP_METHOD"), "POST")
def test_08_smsgateway_success(self): r = add_smtpserver("myServer", "1.2.3.4", sender="*****@*****.**") self.assertTrue(r > 0) smtpmock.setdata(response={"*****@*****.**": (200, "OK")}) identifier = "myMail" provider_module = "privacyidea.lib.smsprovider.SmtpSMSProvider" \ ".SmtpSMSProvider" id = set_smsgateway(identifier, provider_module, description="test", options={"SMTPIDENTIFIER": "myServer", "MAILTO": "*****@*****.**", "SUBJECT": "{phone}", "BODY": "{otp}"}) self.assertTrue(id > 0) sms = create_sms_instance(identifier) r = sms.submit_message("123456", "Halo") self.assertTrue(r)
def test_10_new_smsgateway(self): identifier = "myGW" provider_module = "privacyidea.lib.smsprovider.HttpSMSProvider" \ ".HttpSMSProvider" id = set_smsgateway(identifier, provider_module, description="test", options={"HTTP_METHOD": "POST", "URL": "http://example.com", "RETURN_SUCCESS": "ID", "text": "{otp}", "phone": "{phone}"}) self.assertTrue(id > 0) sms = create_sms_instance(identifier) responses.add(responses.POST, "http://example.com", body=self.success_body) # Here we need to send the SMS r = sms.submit_message("123456", "Hello") self.assertTrue(r) delete_smsgateway(identifier)
def test_03_create_instance_by_identifier(self): # SMS gateway definition identifier = "myGW" provider_module = "privacyidea.lib.smsprovider.HttpSMSProvider" \ ".HttpSMSProvider" id = set_smsgateway(identifier, provider_module, description="test", options={ "HTTP_METHOD": "POST", "URL": "example.com" }, headers={"Authorization": "QWERTZ"}) self.assertTrue(id > 0) sms = create_sms_instance(identifier) self.assertEqual(sms.smsgateway.option_dict.get("URL"), "example.com") self.assertEqual(sms.smsgateway.option_dict.get("HTTP_METHOD"), "POST") self.assertEqual(sms.smsgateway.header_dict.get("Authorization"), "QWERTZ") self.assertEqual(sms.smsgateway.option_dict.get("Authorization"), None)
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 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 _send_sms(self, message="<otp>"): """ send sms :param message: the sms submit message - could contain placeholders like <otp> or <serial> :type message: string :return: submitted message :rtype: string """ if is_true(self.get_tokeninfo("dynamic_phone")): phone = self.user.get_user_phone("mobile") if type(phone) == list and phone: # if there is a non-empty list, we use the first entry phone = phone[0] else: phone = self.get_tokeninfo("phone") if not phone: # pragma: no cover log.warning("Token {0!s} does not have a phone number!".format( self.token.serial)) otp = self.get_otp()[2] serial = self.get_serial() message = message.replace("<otp>", otp) message = message.replace("<serial>", serial) log.debug("sending SMS to phone number {0!s} ".format(phone)) # First we try to get the new SMS gateway config style # The token specific identifier has priority over the system wide identifier sms_gateway_identifier = self.get_tokeninfo( "sms.identifier") or get_from_config("sms.identifier") if sms_gateway_identifier: # New style sms = create_sms_instance(sms_gateway_identifier) else: # Old style (SMSProvider, SMSProviderClass) = self._get_sms_provider() log.debug("smsprovider: {0!s}, class: {1!s}".format( SMSProvider, SMSProviderClass)) try: sms = get_sms_provider_class(SMSProvider, SMSProviderClass)() except Exception as exc: log.error("Failed to load SMSProvider: {0!r}".format(exc)) log.debug("{0!s}".format(traceback.format_exc())) raise exc try: # now we need the config from the env log.debug( "loading SMS configuration for class {0!s}".format(sms)) config = self._get_sms_provider_config() log.debug("config: {0!r}".format(config)) sms.load_config(config) except Exception as exc: log.error( "Failed to load sms.providerConfig: {0!r}".format(exc)) log.debug("{0!s}".format(traceback.format_exc())) raise Exception( "Failed to load sms.providerConfig: {0!r}".format(exc)) log.debug("submitMessage: {0!r}, to phone {1!r}".format( message, phone)) ret = sms.submit_message(phone, message) return ret, message
def _send_sms(self, message="<otp>"): """ send sms :param message: the sms submit message - could contain placeholders like <otp> or <serial> :type message: string :return: submitted message :rtype: string """ if is_true(self.get_tokeninfo("dynamic_phone")): phone = self.user.get_user_phone("mobile") if type(phone) == list and phone: # if there is a non-empty list, we use the first entry phone = phone[0] else: phone = self.get_tokeninfo("phone") if not phone: # pragma: no cover log.warning("Token {0!s} does not have a phone number!".format(self.token.serial)) otp = self.get_otp()[2] serial = self.get_serial() message = message.replace("<otp>", otp) message = message.replace("<serial>", serial) log.debug("sending SMS to phone number {0!s} ".format(phone)) # First we try to get the new SMS gateway config style # The token specific identifier has priority over the system wide identifier sms_gateway_identifier = self.get_tokeninfo("sms.identifier") or get_from_config("sms.identifier") if sms_gateway_identifier: # New style sms = create_sms_instance(sms_gateway_identifier) else: # Old style (SMSProvider, SMSProviderClass) = self._get_sms_provider() log.debug("smsprovider: {0!s}, class: {1!s}".format(SMSProvider, SMSProviderClass)) try: sms = get_sms_provider_class(SMSProvider, SMSProviderClass)() except Exception as exc: log.error("Failed to load SMSProvider: {0!r}".format(exc)) log.debug("{0!s}".format(traceback.format_exc())) raise exc try: # now we need the config from the env log.debug("loading SMS configuration for class {0!s}".format(sms)) config = self._get_sms_provider_config() log.debug("config: {0!r}".format(config)) sms.load_config(config) except Exception as exc: log.error("Failed to load sms.providerConfig: {0!r}".format(exc)) log.debug("{0!s}".format(traceback.format_exc())) raise Exception("Failed to load sms.providerConfig: {0!r}".format(exc)) log.debug("submitMessage: {0!r}, to phone {1!r}".format(message, phone)) ret = sms.submit_message(phone, message) return ret, message
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 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!') return result
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