def test_30_2step_otpkeyformat(self):
     serial = "2step3"
     db_token = Token(serial, tokentype="hotp")
     db_token.save()
     token = HotpTokenClass(db_token)
     token.update({
         "2stepinit": "1",
         "2step_clientsize": "12",
         "hashlib": "sha512",
     })
     self.assertEqual(token.token.rollout_state, "clientwait")
     self.assertEqual(token.get_tokeninfo("2step_clientsize"), "12")
     # fetch the server component for later tests
     server_component = binascii.unhexlify(
         token.token.get_otpkey().getKey())
     # generate a 12-byte client component
     client_component = b'abcdefghijkl'
     checksum = hashlib.sha1(client_component).digest()[:4]
     # wrong checksum
     with warnings.catch_warnings():
         warnings.simplefilter('ignore', category=DeprecationWarning)
         self.assertRaisesRegexp(
             ParameterError, "Incorrect checksum", token.update, {
                 "otpkey":
                 b32encode_and_unicode(b"\x37" + checksum[1:] +
                                       client_component).strip("="),
                 "otpkeyformat":
                 "base32check",
             })
     # construct a secret
     token.update({
         "otpkey":
         b32encode_and_unicode(checksum + client_component).strip("="),
         "otpkeyformat":
         "base32check",
         # the following values are ignored
         "2step_serversize":
         "23",
         "2step_difficulty":
         "666666",
         "2step_clientsize":
         "13"
     })
     # check the generated secret
     secret = binascii.unhexlify(token.token.get_otpkey().getKey())
     # check the correct lengths
     self.assertEqual(len(server_component), 64)  # because of SHA-512
     self.assertEqual(len(client_component), 12)
     self.assertEqual(len(secret), 64)  # because of SHA-512
     # check the secret has been generated according to the specification
     expected_secret = pbkdf2_hmac('sha1',
                                   binascii.hexlify(server_component),
                                   client_component, 10000, len(secret))
     self.assertEqual(secret, expected_secret)
     self.assertTrue(token.token.active)
 def test_30_2step_otpkeyformat(self):
     serial = "2step3"
     db_token = Token(serial, tokentype="hotp")
     db_token.save()
     token = HotpTokenClass(db_token)
     token.update({
         "2stepinit": "1",
         "2step_clientsize": "12",
         "hashlib": "sha512",
     })
     self.assertEqual(token.token.rollout_state, "clientwait")
     self.assertEqual(token.get_tokeninfo("2step_clientsize"), "12")
     # fetch the server component for later tests
     server_component = binascii.unhexlify(token.token.get_otpkey().getKey())
     # generate a 12-byte client component
     client_component = b'abcdefghijkl'
     checksum = hashlib.sha1(client_component).digest()[:4]
     # wrong checksum
     self.assertRaisesRegexp(
         ParameterError,
         "Incorrect checksum",
         token.update,
         {
             "otpkey": b32encode_and_unicode(b"\x37" + checksum[1:] + client_component).strip("="),
             "otpkeyformat": "base32check",
         })
     # construct a secret
     token.update({
         "otpkey": b32encode_and_unicode(checksum + client_component).strip("="),
         "otpkeyformat": "base32check",
         # the following values are ignored
         "2step_serversize": "23",
         "2step_difficulty": "666666",
         "2step_clientsize": "13"
         })
     # check the generated secret
     secret = binascii.unhexlify(token.token.get_otpkey().getKey())
     # check the correct lengths
     self.assertEqual(len(server_component), 64) # because of SHA-512
     self.assertEqual(len(client_component), 12)
     self.assertEqual(len(secret), 64) # because of SHA-512
     # check the secret has been generated according to the specification
     expected_secret = pbkdf2(binascii.hexlify(server_component), client_component, 10000, len(secret))
     self.assertEqual(secret, expected_secret)
     self.assertTrue(token.token.active)
Ejemplo n.º 3
0
    def test_26_conversions(self):
        self.assertEquals(hexlify_and_unicode(u'Hallo'), u'48616c6c6f')
        self.assertEquals(hexlify_and_unicode(b'Hallo'), u'48616c6c6f')
        self.assertEquals(hexlify_and_unicode(b'\x00\x01\x02\xab'), u'000102ab')

        self.assertEquals(b32encode_and_unicode(u'Hallo'), u'JBQWY3DP')
        self.assertEquals(b32encode_and_unicode(b'Hallo'), u'JBQWY3DP')
        self.assertEquals(b32encode_and_unicode(b'\x00\x01\x02\xab'), u'AAAQFKY=')

        self.assertEquals(b64encode_and_unicode(u'Hallo'), u'SGFsbG8=')
        self.assertEquals(b64encode_and_unicode(b'Hallo'), u'SGFsbG8=')
        self.assertEquals(b64encode_and_unicode(b'\x00\x01\x02\xab'), u'AAECqw==')

        self.assertEquals(urlsafe_b64encode_and_unicode(u'Hallo'), u'SGFsbG8=')
        self.assertEquals(urlsafe_b64encode_and_unicode(b'Hallo'), u'SGFsbG8=')
        self.assertEquals(urlsafe_b64encode_and_unicode(b'\x00\x01\x02\xab'), u'AAECqw==')
        self.assertEquals(urlsafe_b64encode_and_unicode(b'\xfa\xfb\xfc\xfd\xfe\xff'),
                          u'-vv8_f7_')
Ejemplo n.º 4
0
    def test_26_conversions(self):
        self.assertEquals(hexlify_and_unicode(u'Hallo'), u'48616c6c6f')
        self.assertEquals(hexlify_and_unicode(b'Hallo'), u'48616c6c6f')
        self.assertEquals(hexlify_and_unicode(b'\x00\x01\x02\xab'), u'000102ab')

        self.assertEquals(b32encode_and_unicode(u'Hallo'), u'JBQWY3DP')
        self.assertEquals(b32encode_and_unicode(b'Hallo'), u'JBQWY3DP')
        self.assertEquals(b32encode_and_unicode(b'\x00\x01\x02\xab'), u'AAAQFKY=')

        self.assertEquals(b64encode_and_unicode(u'Hallo'), u'SGFsbG8=')
        self.assertEquals(b64encode_and_unicode(b'Hallo'), u'SGFsbG8=')
        self.assertEquals(b64encode_and_unicode(b'\x00\x01\x02\xab'), u'AAECqw==')

        self.assertEquals(urlsafe_b64encode_and_unicode(u'Hallo'), u'SGFsbG8=')
        self.assertEquals(urlsafe_b64encode_and_unicode(b'Hallo'), u'SGFsbG8=')
        self.assertEquals(urlsafe_b64encode_and_unicode(b'\x00\x01\x02\xab'), u'AAECqw==')
        self.assertEquals(urlsafe_b64encode_and_unicode(b'\xfa\xfb\xfc\xfd\xfe\xff'),
                          u'-vv8_f7_')
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
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 test_04_api_authenticate_smartphone(self):
        # Test the /validate/check endpoints and the smartphone endpoint /ttype/push
        # for authentication

        # get enrolled push token
        toks = get_tokens(tokentype="push")
        self.assertEqual(len(toks), 1)
        tokenobj = toks[0]

        # set PIN
        tokenobj.set_pin("pushpin")
        tokenobj.add_user(User("cornelius", self.realm1))

        def check_firebase_params(request):
            payload = json.loads(request.body)
            # check the signature in the payload!
            data = payload.get("message").get("data")

            sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format(
                **data)
            token_obj = get_tokens(serial=data.get("serial"))[0]
            pem_pubkey = token_obj.get_tokeninfo(PUBLIC_KEY_SERVER)
            pubkey_obj = load_pem_public_key(to_bytes(pem_pubkey),
                                             backend=default_backend())
            signature = b32decode(data.get("signature"))
            # If signature does not match it will raise InvalidSignature exception
            pubkey_obj.verify(signature, sign_string.encode("utf8"),
                              padding.PKCS1v15(), hashes.SHA256())
            headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'}
            return (200, headers, json.dumps({}))

        # We mock the ServiceAccountCredentials, since we can not directly contact the Google API
        with mock.patch(
                'privacyidea.lib.smsprovider.FirebaseProvider.ServiceAccountCredentials'
        ) as mySA:
            # alternative: side_effect instead of return_value
            mySA.from_json_keyfile_name.return_value = myCredentials(
                myAccessTokenInfo("my_bearer_token"))

            # add responses, to simulate the communication to firebase
            responses.add_callback(
                responses.POST,
                'https://fcm.googleapis.com/v1/projects/test-123456/messages:send',
                callback=check_firebase_params,
                content_type="application/json")

            # Send the first authentication request to trigger the challenge
            with self.app.test_request_context('/validate/check',
                                               method='POST',
                                               data={
                                                   "user": "******",
                                                   "realm": self.realm1,
                                                   "pass": "******"
                                               }):
                res = self.app.full_dispatch_request()
                self.assertTrue(res.status_code == 200, res)
                jsonresp = res.json
                self.assertFalse(jsonresp.get("result").get("value"))
                self.assertTrue(jsonresp.get("result").get("status"))
                self.assertEqual(
                    jsonresp.get("detail").get("serial"),
                    tokenobj.token.serial)
                self.assertTrue("transaction_id" in jsonresp.get("detail"))
                transaction_id = jsonresp.get("detail").get("transaction_id")
                self.assertEqual(
                    jsonresp.get("detail").get("message"),
                    DEFAULT_CHALLENGE_TEXT)

            # Our ServiceAccountCredentials mock has not been called because we use a cached token
            self.assertEqual(len(mySA.from_json_keyfile_name.mock_calls), 0)
            self.assertIn(FIREBASE_FILE,
                          get_app_local_store()["firebase_token"])

        # The challenge is sent to the smartphone via the Firebase service, so we do not know
        # the challenge from the /validate/check API.
        # So lets read the challenge from the database!

        challengeobject_list = get_challenges(serial=tokenobj.token.serial,
                                              transaction_id=transaction_id)
        challenge = challengeobject_list[0].challenge

        # Incomplete request fails with HTTP400
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={
                                               "serial": tokenobj.token.serial,
                                               "nonce": challenge
                                           }):
            res = self.app.full_dispatch_request()
            self.assertEquals(res.status_code, 400)

        # This is what the smartphone answers.
        # create the signature:
        sign_data = "{0!s}|{1!s}".format(challenge, tokenobj.token.serial)
        signature = b32encode_and_unicode(
            self.smartphone_private_key.sign(sign_data.encode("utf-8"),
                                             padding.PKCS1v15(),
                                             hashes.SHA256()))
        # Try an invalid signature first
        wrong_sign_data = "{}|{}".format(challenge, tokenobj.token.serial[1:])
        wrong_signature = b32encode_and_unicode(
            self.smartphone_private_key.sign(wrong_sign_data.encode("utf-8"),
                                             padding.PKCS1v15(),
                                             hashes.SHA256()))
        # Signed the wrong data
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={
                                               "serial": tokenobj.token.serial,
                                               "nonce": challenge,
                                               "signature": wrong_signature
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Correct signature, wrong challenge
        wrong_challenge = b32encode_and_unicode(geturandom())
        wrong_sign_data = "{}|{}".format(wrong_challenge,
                                         tokenobj.token.serial)
        wrong_signature = b32encode_and_unicode(
            self.smartphone_private_key.sign(wrong_sign_data.encode("utf-8"),
                                             padding.PKCS1v15(),
                                             hashes.SHA256()))
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={
                                               "serial": tokenobj.token.serial,
                                               "nonce": wrong_challenge,
                                               "signature": wrong_signature
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Correct signature, empty nonce
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={
                                               "serial": tokenobj.token.serial,
                                               "nonce": "",
                                               "signature": signature
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Correct signature, wrong private key
        wrong_key = rsa.generate_private_key(public_exponent=65537,
                                             key_size=4096,
                                             backend=default_backend())
        wrong_sign_data = "{}|{}".format(challenge, tokenobj.token.serial)
        wrong_signature = b32encode_and_unicode(
            wrong_key.sign(wrong_sign_data.encode("utf-8"), padding.PKCS1v15(),
                           hashes.SHA256()))
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={
                                               "serial": tokenobj.token.serial,
                                               "nonce": challenge,
                                               "signature": wrong_signature
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Result value is still false
        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={
                                               "user": "******",
                                               "realm": self.realm1,
                                               "pass": "",
                                               "state": transaction_id
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertFalse(res.json['result']['value'])

        # Now the correct request
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={
                                               "serial": tokenobj.token.serial,
                                               "nonce": challenge,
                                               "signature": signature
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertTrue(res.json['result']['value'])

        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={
                                               "user": "******",
                                               "realm": self.realm1,
                                               "pass": "",
                                               "state": transaction_id
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            jsonresp = res.json
            # Result-Value is True
            self.assertTrue(jsonresp.get("result").get("value"))
Ejemplo n.º 8
0
    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
Ejemplo n.º 9
0
    def get_init_detail(self, params=None, user=None):
        """
        to complete the token initialization some additional details
        should be returned, which are displayed at the end of
        the token initialization.
        This is the e.g. the enrollment URL for a Google Authenticator.
        """
        response_detail = TokenClass.get_init_detail(self, params, user)
        params = params or {}
        tokenlabel = params.get("tokenlabel", "<s>")
        tokenissuer = params.get("tokenissuer", "privacyIDEA")
        # If the init_details contain an OTP key the OTP key
        # should be displayed as an enrollment URL
        otpkey = self.init_details.get('otpkey')
        # Add rollout state the response
        response_detail['rollout_state'] = self.token.rollout_state
        # Add two-step initialization parameters to response and QR code
        extra_data = {}
        if is_true(params.get("2stepinit")):
            twostep_parameters = self._get_twostep_parameters()
            extra_data.update(twostep_parameters)
            response_detail.update(twostep_parameters)
        imageurl = params.get("appimageurl")
        if imageurl:
            extra_data.update({"image": imageurl})
        force_app_pin = params.get('force_app_pin')
        if force_app_pin:
            extra_data.update({'pin': True})
        if otpkey:
            tok_type = self.type.lower()
            if user is not None:
                try:
                    key_bin = binascii.unhexlify(otpkey)
                    # also strip the padding =, as it will get problems with the google app.
                    value_b32_str = b32encode_and_unicode(key_bin).strip('=')
                    response_detail["otpkey"]["value_b32"] = value_b32_str
                    goo_url = cr_google(key=otpkey,
                                        user=user.login,
                                        realm=user.realm,
                                        tokentype=tok_type.lower(),
                                        serial=self.get_serial(),
                                        tokenlabel=tokenlabel,
                                        hash_algo=params.get(
                                            "hashlib", "sha1"),
                                        digits=params.get("otplen", 6),
                                        period=params.get("timeStep", 30),
                                        issuer=tokenissuer,
                                        user_obj=user,
                                        extra_data=extra_data)
                    response_detail["googleurl"] = {
                        "description": _("URL for google "
                                         "Authenticator"),
                        "value": goo_url,
                        "img": create_img(goo_url, width=250)
                    }

                    oath_url = cr_oath(otpkey=otpkey,
                                       user=user.login,
                                       realm=user.realm,
                                       type=tok_type,
                                       serial=self.get_serial(),
                                       tokenlabel=tokenlabel,
                                       extra_data=extra_data)
                    response_detail["oathurl"] = {
                        "description": _("URL for"
                                         " OATH "
                                         "token"),
                        "value": oath_url,
                        "img": create_img(oath_url, width=250)
                    }
                except Exception as ex:  # pragma: no cover
                    log.error("{0!s}".format((traceback.format_exc())))
                    log.error(
                        'failed to set oath or google url: {0!r}'.format(ex))

        return response_detail
Ejemplo n.º 10
0
    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
Ejemplo n.º 11
0
    def get_init_detail(self, params=None, user=None):
        """
        to complete the token initialization some additional details
        should be returned, which are displayed at the end of
        the token initialization.
        This is the e.g. the enrollment URL for a Google Authenticator.
        """
        response_detail = TokenClass.get_init_detail(self, params, user)
        params = params or {}
        tokenlabel = params.get("tokenlabel", "<s>")
        tokenissuer = params.get("tokenissuer", "privacyIDEA")
        # If the init_details contain an OTP key the OTP key
        # should be displayed as an enrollment URL
        otpkey = self.init_details.get('otpkey')
        # Add rollout state the response
        response_detail['rollout_state'] = self.token.rollout_state
        # Add two-step initialization parameters to response and QR code
        extra_data = {}
        if is_true(params.get("2stepinit")):
            twostep_parameters = self._get_twostep_parameters()
            extra_data.update(twostep_parameters)
            response_detail.update(twostep_parameters)
        imageurl = params.get("appimageurl")
        if imageurl:
            extra_data.update({"image": imageurl})
        if otpkey:
            tok_type = self.type.lower()
            if user is not None:                               
                try:
                    key_bin = binascii.unhexlify(otpkey)
                    # also strip the padding =, as it will get problems with the google app.
                    value_b32_str = b32encode_and_unicode(key_bin).strip('=')
                    response_detail["otpkey"]["value_b32"] = value_b32_str
                    goo_url = cr_google(key=otpkey,
                                        user=user.login,
                                        realm=user.realm,
                                        tokentype=tok_type.lower(),
                                        serial=self.get_serial(),
                                        tokenlabel=tokenlabel,
                                        hash_algo=params.get("hashlib", "sha1"),
                                        digits=params.get("otplen", 6),
                                        period=params.get("timeStep", 30),
                                        issuer=tokenissuer,
                                        user_obj=user,
                                        extra_data=extra_data)
                    response_detail["googleurl"] = {"description":
                                                    _("URL for google "
                                                      "Authenticator"),
                                                    "value": goo_url,
                                                    "img": create_img(goo_url,
                                                                      width=250)
                                                    }

                    oath_url = cr_oath(otpkey=otpkey,
                                       user=user.login,
                                       realm=user.realm,
                                       type=tok_type,
                                       serial=self.get_serial(),
                                       tokenlabel=tokenlabel,
                                       extra_data=extra_data)
                    response_detail["oathurl"] = {"description": _("URL for"
                                                                   " OATH "
                                                                   "token"),
                                                  "value": oath_url,
                                                  "img": create_img(oath_url,
                                                                    width=250)
                                                  }
                except Exception as ex:  # pragma: no cover
                    log.error("{0!s}".format((traceback.format_exc())))
                    log.error('failed to set oath or google url: {0!r}'.format(ex))

        return response_detail
Ejemplo n.º 12
0
    def test_04_api_authenticate_smartphone(self):
        # Test the /validate/check endpoints and the smartphone endpoint /ttype/push
        # for authentication

        # get enrolled push token
        toks = get_tokens(tokentype="push")
        self.assertEqual(len(toks), 1)
        tokenobj = toks[0]

        # set PIN
        tokenobj.set_pin("pushpin")
        tokenobj.add_user(User("cornelius", self.realm1))

        def check_firebase_params(request):
            payload = json.loads(request.body)
            # check the signature in the payload!
            data = payload.get("message").get("data")

            sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format(**data)
            token_obj = get_tokens(serial=data.get("serial"))[0]
            pem_pubkey = token_obj.get_tokeninfo(PUBLIC_KEY_SERVER)
            pubkey_obj = load_pem_public_key(to_bytes(pem_pubkey), backend=default_backend())
            signature = b32decode(data.get("signature"))
            # If signature does not match it will raise InvalidSignature exception
            pubkey_obj.verify(signature, sign_string.encode("utf8"),
                              padding.PKCS1v15(),
                              hashes.SHA256())
            headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'}
            return (200, headers, json.dumps({}))

        # We mock the ServiceAccountCredentials, since we can not directly contact the Google API
        with mock.patch('privacyidea.lib.smsprovider.FirebaseProvider.ServiceAccountCredentials') as mySA:
            # alternative: side_effect instead of return_value
            mySA.from_json_keyfile_name.return_value = myCredentials(myAccessTokenInfo("my_bearer_token"))

            # add responses, to simulate the communication to firebase
            responses.add_callback(responses.POST, 'https://fcm.googleapis.com/v1/projects/test-123456/messages:send',
                          callback=check_firebase_params,
                          content_type="application/json")

            # Send the first authentication request to trigger the challenge
            with self.app.test_request_context('/validate/check',
                                               method='POST',
                                               data={"user": "******",
                                                     "realm": self.realm1,
                                                     "pass": "******"}):
                res = self.app.full_dispatch_request()
                self.assertTrue(res.status_code == 200, res)
                jsonresp = json.loads(res.data.decode('utf8'))
                self.assertFalse(jsonresp.get("result").get("value"))
                self.assertTrue(jsonresp.get("result").get("status"))
                self.assertEqual(jsonresp.get("detail").get("serial"), tokenobj.token.serial)
                self.assertTrue("transaction_id" in jsonresp.get("detail"))
                transaction_id = jsonresp.get("detail").get("transaction_id")
                self.assertEqual(jsonresp.get("detail").get("message"), DEFAULT_CHALLENGE_TEXT)

        # The challenge is sent to the smartphone via the Firebase service, so we do not know
        # the challenge from the /validate/check API.
        # So lets read the challenge from the database!

        challengeobject_list = get_challenges(serial=tokenobj.token.serial,
                                              transaction_id=transaction_id)
        challenge = challengeobject_list[0].challenge

        # Incomplete request fails with HTTP400
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={"serial": tokenobj.token.serial,
                                                 "nonce": challenge}):
            res = self.app.full_dispatch_request()
            self.assertEquals(res.status_code, 400)

        # This is what the smartphone answers.
        # create the signature:
        sign_data = "{0!s}|{1!s}".format(challenge, tokenobj.token.serial)
        signature = b32encode_and_unicode(
            self.smartphone_private_key.sign(sign_data.encode("utf-8"),
                                             padding.PKCS1v15(),
                                             hashes.SHA256()))
        # Try an invalid signature first
        wrong_sign_data = "{}|{}".format(challenge, tokenobj.token.serial[1:])
        wrong_signature = b32encode_and_unicode(
            self.smartphone_private_key.sign(wrong_sign_data.encode("utf-8"),
                                             padding.PKCS1v15(),
                                             hashes.SHA256()))
        # Signed the wrong data
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={"serial": tokenobj.token.serial,
                                                 "nonce": challenge,
                                                 "signature": wrong_signature}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Correct signature, wrong challenge
        wrong_challenge = b32encode_and_unicode(geturandom())
        wrong_sign_data = "{}|{}".format(wrong_challenge, tokenobj.token.serial)
        wrong_signature = b32encode_and_unicode(
            self.smartphone_private_key.sign(wrong_sign_data.encode("utf-8"),
                                             padding.PKCS1v15(),
                                             hashes.SHA256()))
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={"serial": tokenobj.token.serial,
                                                 "nonce": wrong_challenge,
                                                 "signature": wrong_signature}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Correct signature, empty nonce
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={"serial": tokenobj.token.serial,
                                                 "nonce": "",
                                                 "signature": signature}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Correct signature, wrong private key
        wrong_key = rsa.generate_private_key(public_exponent=65537,
                                             key_size=4096,
                                             backend=default_backend())
        wrong_sign_data = "{}|{}".format(challenge, tokenobj.token.serial)
        wrong_signature = b32encode_and_unicode(
            wrong_key.sign(wrong_sign_data.encode("utf-8"),
                           padding.PKCS1v15(),
                           hashes.SHA256()))
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={"serial": tokenobj.token.serial,
                                                 "nonce": challenge,
                                                 "signature": wrong_signature}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Result value is still false
        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={"user": "******",
                                                 "realm": self.realm1,
                                                 "pass": "",
                                                 "state": transaction_id}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertFalse(res.json['result']['value'])

        # Now the correct request
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={"serial": tokenobj.token.serial,
                                                 "nonce": challenge,
                                                 "signature": signature}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertTrue(res.json['result']['value'])

        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={"user": "******",
                                                 "realm": self.realm1,
                                                 "pass": "",
                                                 "state": transaction_id}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            jsonresp = json.loads(res.data.decode('utf8'))
            # Result-Value is True
            self.assertTrue(jsonresp.get("result").get("value"))
Ejemplo n.º 13
0
    def test_01_init_token(self):
        set_policy(
            name="allow_2step",
            action=["hotp_2step=allow", "enrollHOTP=1", "delete"],
            scope=SCOPE.ADMIN,
        )
        with self.app.test_request_context('/token/init',
                                           method='POST',
                                           data={
                                               "type": "hotp",
                                               "genkey": "1",
                                               "2stepinit": "1"
                                           },
                                           headers={'Authorization': self.at}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            result = res.json.get("result")
            self.assertTrue(result.get("status") is True, result)
            self.assertTrue(result.get("value") is True, result)
            detail = res.json.get("detail")
            serial = detail.get("serial")
            otpkey_url = detail.get("otpkey", {}).get("value")
            server_component = binascii.unhexlify(otpkey_url.split("/")[2])
            google_url = detail["googleurl"]["value"]
            self.assertIn('2step_difficulty=10000', google_url)
            self.assertIn('2step_salt=8', google_url)
            self.assertIn('2step_output=20', google_url)
            self.assertEqual(detail['2step_difficulty'], 10000)
            self.assertEqual(detail['2step_salt'], 8)
            self.assertEqual(detail['2step_output'], 20)

        # Do a 2step 1st step on an already existing token using the rollover parameter will succeed
        with self.app.test_request_context('/token/init',
                                           method='POST',
                                           data={
                                               "type": "hotp",
                                               "genkey": "1",
                                               "rollover": "1",
                                               "serial": serial,
                                               "2stepinit": "1"
                                           },
                                           headers={'Authorization': self.at}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            result = res.json.get("result")
            self.assertTrue(result.get("status") is True, result)
            self.assertTrue(result.get("value") is True, result)
            detail = res.json.get("detail")
            serial = detail.get("serial")
            otpkey_url = detail.get("otpkey", {}).get("value")
            server_component = binascii.unhexlify(otpkey_url.split("/")[2])
            google_url = detail["googleurl"]["value"]
            self.assertIn('2step_difficulty=10000', google_url)
            self.assertIn('2step_salt=8', google_url)
            self.assertIn('2step_output=20', google_url)
            self.assertEqual(detail['2step_difficulty'], 10000)
            self.assertEqual(detail['2step_salt'], 8)
            self.assertEqual(detail['2step_output'], 20)

        client_component = b"VRYSECRT"
        checksum = hashlib.sha1(client_component).digest()[:4]
        base32check_client_component = b32encode_and_unicode(
            checksum + client_component).strip("=")

        # Try to do a 2stepinit on a second step without rollover parameter will raise an error
        with self.app.test_request_context('/token/init',
                                           method='POST',
                                           data={
                                               "type": "hotp",
                                               "2stepinit": "1",
                                               "serial": serial,
                                               "otpkey":
                                               base32check_client_component,
                                               "otpkeyformat": "base32check"
                                           },
                                           headers={'Authorization': self.at}):
            res = self.app.full_dispatch_request()
            self.assertEqual(res.status_code, 400)
            result = res.json.get("result")
            self.assertIn(
                '2stepinit is only to be used in the first initialization step',
                result.get("error").get("message"))

        # Invalid base32check will raise an error
        with self.app.test_request_context(
                '/token/init',
                method='POST',
                data={
                    "type": "hotp",
                    "2stepinit": "1",
                    "serial": serial,
                    "otpkey": "A" + base32check_client_component[1:],
                    "otpkeyformat": "base32check"
                },
                headers={'Authorization': self.at}):
            res = self.app.full_dispatch_request()
            self.assertEqual(res.status_code, 400)
            result = res.json.get("result")
            self.assertIn('Malformed base32check data: Incorrect checksum',
                          result.get("error").get("message"))

        # Authentication does not work yet!
        wrong_otp_value = HmacOtp().generate(key=server_component, counter=1)
        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={
                                               "serial": serial,
                                               "pass": wrong_otp_value
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            result = res.json
            self.assertTrue(result.get("result").get("status"))
            self.assertFalse(result.get("result").get("value"))
            self.assertEqual(
                result.get("detail").get("message"),
                u'matching 1 tokens, Token is disabled')

        # Now doing the correct 2nd step
        with self.app.test_request_context('/token/init',
                                           method='POST',
                                           data={
                                               "type": "hotp",
                                               "serial": serial,
                                               "otpkey":
                                               base32check_client_component,
                                               "otpkeyformat": "base32check"
                                           },
                                           headers={'Authorization': self.at}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            result = res.json.get("result")
            self.assertTrue(result.get("status") is True, result)
            self.assertTrue(result.get("value") is True, result)
            detail = res.json.get("detail")
            otpkey_url = detail.get("otpkey", {}).get("value")
            otpkey = otpkey_url.split("/")[2]
            self.assertNotIn('2step', detail)

        # Now try to authenticate
        otpkey_bin = binascii.unhexlify(otpkey)
        otp_value = HmacOtp().generate(key=otpkey_bin, counter=1)
        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={
                                               "serial": serial,
                                               "pass": otp_value
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            result = res.json.get("result")
            self.assertEqual(result.get("status"), True)
            self.assertEqual(result.get("value"), True)

        # Check that the OTP key is what we expected it to be
        expected_secret = pbkdf2_hmac('sha1',
                                      binascii.hexlify(server_component),
                                      client_component, 10000, 20)
        self.assertEqual(otpkey_bin, expected_secret)

        with self.app.test_request_context('/token/' + serial,
                                           method='DELETE',
                                           headers={'Authorization': self.at}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
        delete_policy("allow_2step")
Ejemplo n.º 14
0
def create_google_authenticator_url(key=None,
                                    user=None,
                                    realm=None,
                                    tokentype="hotp",
                                    period=30,
                                    serial="mylabel",
                                    tokenlabel="<s>",
                                    hash_algo="SHA1",
                                    digits="6",
                                    issuer="privacyIDEA",
                                    user_obj=None,
                                    extra_data=None):
    """
    This creates the google authenticator URL.
    This url may only be 119 characters long.
    If the URL would be longer, we shorten the username

    We expect the key to be hexlified!
    """
    extra_data = extra_data or {}

    # policy depends on some lib.util

    user_obj = user_obj or User()
    if tokentype.lower() == "hotp":
        tokentype = "hotp"
        counter = "counter=1&"
    else:
        counter = ""

    # We need realm und user to be a string
    realm = realm or ""
    user = user or ""

    key_bin = binascii.unhexlify(key)
    # also strip the padding =, as it will get problems with the google app.
    otpkey = b32encode_and_unicode(key_bin).strip('=')

    base_len = len("otpauth://{0!s}/?secret={1!s}&counter=1".format(
        tokentype, otpkey))
    allowed_label_len = MAX_QRCODE_LEN - base_len
    log.debug("we have got {0!s} characters left for the token label".format(
        str(allowed_label_len)))
    # Deprecated
    label = tokenlabel.replace("<s>",
                               serial).replace("<u>",
                                               user).replace("<r>", realm)
    label = label.format(serial=serial,
                         user=user,
                         realm=realm,
                         givenname=user_obj.info.get("givenname", ""),
                         surname=user_obj.info.get("surname", ""))

    issuer = issuer.format(serial=serial,
                           user=user,
                           realm=realm,
                           givenname=user_obj.info.get("givenname", ""),
                           surname=user_obj.info.get("surname", ""))

    label = label[0:allowed_label_len]
    url_label = quote(label.encode("utf-8"))
    url_issuer = quote(issuer.encode("utf-8"))

    if hash_algo.lower() != "sha1":
        hash_algo = "algorithm={0!s}&".format(hash_algo)
    else:
        # If the hash_algo is SHA1, we do not add it to the QR code to keep
        # the QR code simpler
        hash_algo = ""

    if tokentype.lower() == "totp":
        period = "period={0!s}&".format(period)
    else:
        period = ""

    return ("otpauth://{tokentype!s}/{label!s}?secret={secret!s}&"
            "{counter!s}{hash!s}{period!s}"
            "digits={digits!s}&"
            "issuer={issuer!s}{extra}".format(
                tokentype=tokentype,
                label=url_label,
                secret=otpkey,
                hash=hash_algo,
                period=period,
                digits=digits,
                issuer=url_issuer,
                counter=counter,
                extra=_construct_extra_parameters(extra_data)))
Ejemplo n.º 15
0
def create_google_authenticator_url(key=None, user=None,
                                    realm=None, tokentype="hotp", period=30,
                                    serial="mylabel", tokenlabel="<s>",
                                    hash_algo="SHA1", digits="6",
                                    issuer="privacyIDEA", user_obj=None,
                                    extra_data=None):
    """
    This creates the google authenticator URL.
    This url may only be 119 characters long.
    If the URL would be longer, we shorten the username

    We expect the key to be hexlified!
    """
    extra_data = extra_data or {}

    # policy depends on some lib.util

    user_obj = user_obj or User()
    if tokentype.lower() == "hotp":
        tokentype = "hotp"
        counter = "counter=1&"
    else:
        counter = ""

    # We need realm und user to be a string
    realm = realm or ""
    user = user or ""

    key_bin = binascii.unhexlify(key)
    # also strip the padding =, as it will get problems with the google app.
    otpkey = b32encode_and_unicode(key_bin).strip('=')

    base_len = len("otpauth://{0!s}/?secret={1!s}&counter=1".format(tokentype, otpkey))
    allowed_label_len = MAX_QRCODE_LEN - base_len
    log.debug("we have got {0!s} characters left for the token label".format(
              str(allowed_label_len)))
    # Deprecated
    label = tokenlabel.replace("<s>",
                               serial).replace("<u>",
                                               user).replace("<r>", realm)
    label = label.format(serial=serial, user=user, realm=realm,
                         givenname=user_obj.info.get("givenname", ""),
                         surname=user_obj.info.get("surname", ""))

    issuer = issuer.format(serial=serial, user=user, realm=realm,
                           givenname=user_obj.info.get("givenname", ""),
                           surname=user_obj.info.get("surname", ""))

    label = label[0:allowed_label_len]
    url_label = quote(label.encode("utf-8"))
    url_issuer = quote(issuer.encode("utf-8"))

    if hash_algo.lower() != "sha1":
        hash_algo = "algorithm={0!s}&".format(hash_algo)
    else:
        # If the hash_algo is SHA1, we do not add it to the QR code to keep
        # the QR code simpler
        hash_algo = ""

    if tokentype.lower() == "totp":
        period = "period={0!s}&".format(period)
    else:
        period = ""

    return ("otpauth://{tokentype!s}/{label!s}?secret={secret!s}&"
            "{counter!s}{hash!s}{period!s}"
            "digits={digits!s}&"
            "issuer={issuer!s}{extra}".format(tokentype=tokentype,
                                       label=url_label, secret=otpkey,
                                       hash=hash_algo, period=period,
                                       digits=digits, issuer=url_issuer,
                                       counter=counter,
                                       extra=_construct_extra_parameters(extra_data)))
Ejemplo n.º 16
0
    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
Ejemplo n.º 17
0
    def test_02_api_push_poll(self):
        r = set_smsgateway(
            self.firebase_config_name,
            u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider',
            "myFB", FB_CONFIG_VALS)
        self.assertGreater(r, 0)

        # create a new push token
        tokenobj = self._create_push_token()
        serial = tokenobj.get_serial()

        # set PIN
        tokenobj.set_pin("pushpin")
        tokenobj.add_user(User("cornelius", self.realm1))

        # We mock the ServiceAccountCredentials, since we can not directly contact the Google API
        with mock.patch(
                'privacyidea.lib.smsprovider.FirebaseProvider.service_account.Credentials'
                '.from_service_account_file') as mySA:
            # alternative: side_effect instead of return_value
            mySA.return_value = _create_credential_mock()

            # add responses, to simulate the communication to firebase
            responses.add(
                responses.POST,
                'https://fcm.googleapis.com/v1/projects/test-123456/messages:send',
                body="""{}""",
                content_type="application/json")

            # Send the first authentication request to trigger the challenge.
            # No push notification is submitted to firebase, but a challenge is created anyway
            with self.app.test_request_context('/validate/check',
                                               method='POST',
                                               data={
                                                   "user": "******",
                                                   "realm": self.realm1,
                                                   "pass": "******"
                                               }):
                res = self.app.full_dispatch_request()
                self.assertTrue(res.status_code == 400, res)
                jsonresp = res.json
                self.assertFalse(jsonresp.get("result").get("status"))
                self.assertEqual(
                    jsonresp.get("result").get("error").get("code"), 401)
                self.assertEqual(
                    jsonresp.get("result").get("error").get("message"),
                    "ERR401: Failed to submit message to Firebase service.")

        # first create a signature
        ts = datetime.utcnow().isoformat()
        sign_string = u"{serial}|{timestamp}".format(serial=serial,
                                                     timestamp=ts)
        sig = self.smartphone_private_key.sign(sign_string.encode('utf8'),
                                               padding.PKCS1v15(),
                                               hashes.SHA256())
        # now check that we receive the challenge when polling
        with self.app.test_request_context('/ttype/push',
                                           method='GET',
                                           data={
                                               "serial": serial,
                                               "timestamp": ts,
                                               "signature": b32encode(sig)
                                           }):
            res = self.app.full_dispatch_request()

            # check that the serial was set in flask g (via before_request in ttype.py)
            self.assertTrue(self.app_context.g.serial, serial)
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            chall = res.json['result']['value'][0]
            self.assertTrue(chall)

            challenge = chall["nonce"]
            # This is what the smartphone answers.
            # create the signature:
            sign_data = "{0!s}|{1!s}".format(challenge, serial)
            signature = b32encode_and_unicode(
                self.smartphone_private_key.sign(sign_data.encode("utf-8"),
                                                 padding.PKCS1v15(),
                                                 hashes.SHA256()))

            # Answer the challenge
            with self.app.test_request_context('/ttype/push',
                                               method='POST',
                                               data={
                                                   "serial": serial,
                                                   "nonce": challenge,
                                                   "signature": signature
                                               }):
                res = self.app.full_dispatch_request()
                self.assertTrue(res.status_code == 200, res)
                self.assertTrue(res.json['result']['status'])
                self.assertTrue(res.json['result']['value'])
Ejemplo n.º 18
0
    def test_01_init_token(self):
        set_policy(
            name="allow_2step",
            action=["hotp_2step=allow", "enrollHOTP=1", "delete"],
            scope=SCOPE.ADMIN,
        )
        with self.app.test_request_context('/token/init',
                                           method='POST',
                                           data={"type": "hotp",
                                                 "genkey": "1",
                                                 "2stepinit": "1"},
                                           headers={'Authorization': self.at}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            result = json.loads(res.data.decode('utf8')).get("result")
            self.assertTrue(result.get("status") is True, result)
            self.assertTrue(result.get("value") is True, result)
            detail = json.loads(res.data.decode('utf8')).get("detail")
            serial = detail.get("serial")
            otpkey_url = detail.get("otpkey", {}).get("value")
            server_component = binascii.unhexlify(otpkey_url.split("/")[2])
            google_url = detail["googleurl"]["value"]
            self.assertIn('2step_difficulty=10000', google_url)
            self.assertIn('2step_salt=8', google_url)
            self.assertIn('2step_output=20', google_url)
            self.assertEqual(detail['2step_difficulty'], 10000)
            self.assertEqual(detail['2step_salt'], 8)
            self.assertEqual(detail['2step_output'], 20)

        client_component = b"VRYSECRT"
        checksum = hashlib.sha1(client_component).digest()[:4]
        base32check_client_component = b32encode_and_unicode(checksum + client_component).strip("=")

        # Try to do a 2stepinit on a second step will raise an error
        with self.app.test_request_context('/token/init',
                                           method='POST',
                                           data={"type": "hotp",
                                                 "2stepinit": "1",
                                                 "serial": serial,
                                                 "otpkey": base32check_client_component,
                                                 "otpkeyformat": "base32check"},
                                           headers={'Authorization': self.at}):
            res = self.app.full_dispatch_request()
            self.assertEqual(res.status_code, 400)
            result = json.loads(res.data.decode('utf8')).get("result")
            self.assertIn('2stepinit is only to be used in the first initialization step',
                          result.get("error").get("message"))

        # Invalid base32check will raise an error
        with self.app.test_request_context('/token/init',
                                           method='POST',
                                           data={"type": "hotp",
                                                 "2stepinit": "1",
                                                 "serial": serial,
                                                 "otpkey": "A" + base32check_client_component[1:],
                                                 "otpkeyformat": "base32check"},
                                           headers={'Authorization': self.at}):
            res = self.app.full_dispatch_request()
            self.assertEqual(res.status_code, 400)
            result = json.loads(res.data.decode('utf8')).get("result")
            self.assertIn('Malformed base32check data: Incorrect checksum',
                          result.get("error").get("message"))

        # Authentication does not work yet!
        wrong_otp_value = HmacOtp().generate(key=server_component, counter=1)
        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={"serial": serial,
                                                 "pass": wrong_otp_value}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            result = json.loads(res.data.decode('utf8'))
            self.assertTrue(result.get("result").get("status"))
            self.assertFalse(result.get("result").get("value"))
            self.assertEqual(result.get("detail").get("message"),
                             u'matching 1 tokens, Token is disabled')

        # Now doing the correct 2nd step
        with self.app.test_request_context('/token/init',
                                           method='POST',
                                           data={"type": "hotp",
                                                 "serial": serial,
                                                 "otpkey": base32check_client_component,
                                                 "otpkeyformat": "base32check"},
                                           headers={'Authorization': self.at}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            result = json.loads(res.data.decode('utf8')).get("result")
            self.assertTrue(result.get("status") is True, result)
            self.assertTrue(result.get("value") is True, result)
            detail = json.loads(res.data.decode('utf8')).get("detail")
            otpkey_url = detail.get("otpkey", {}).get("value")
            otpkey = otpkey_url.split("/")[2]
            self.assertNotIn('2step', detail)

        # Now try to authenticate
        otpkey_bin = binascii.unhexlify(otpkey)
        otp_value = HmacOtp().generate(key=otpkey_bin, counter=1)
        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={"serial": serial,
                                                 "pass": otp_value}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            result = json.loads(res.data.decode('utf8')).get("result")
            self.assertEqual(result.get("status"), True)
            self.assertEqual(result.get("value"), True)

        # Check that the OTP key is what we expected it to be
        expected_secret = pbkdf2(binascii.hexlify(server_component), client_component, 10000, 20)
        self.assertEqual(otpkey_bin, expected_secret)

        with self.app.test_request_context('/token/'+ serial,
                                           method='DELETE',
                                           headers={'Authorization': self.at}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
        delete_policy("allow_2step")
Ejemplo n.º 19
0
    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 test_06_api_auth(self):
        self.setUp_user_realms()

        # get enrolled push token
        toks = get_tokens(tokentype="push")
        self.assertEqual(len(toks), 1)
        tokenobj = toks[0]

        # set PIN
        tokenobj.set_pin("pushpin")
        tokenobj.add_user(User("cornelius", self.realm1))

        # Set a loginmode policy
        set_policy("webui",
                   scope=SCOPE.WEBUI,
                   action="{}={}".format(ACTION.LOGINMODE,
                                         LOGINMODE.PRIVACYIDEA))
        # Set a PUSH_WAIT action which will be ignored by privacyIDEA
        set_policy("push1",
                   scope=SCOPE.AUTH,
                   action="{0!s}=20".format(PUSH_ACTION.WAIT))
        with mock.patch(
                'privacyidea.lib.smsprovider.FirebaseProvider.ServiceAccountCredentials'
        ) as mySA:
            # alternative: side_effect instead of return_value
            mySA.from_json_keyfile_name.return_value = myCredentials(
                myAccessTokenInfo("my_bearer_token"))

            # add responses, to simulate the communication to firebase
            responses.add(
                responses.POST,
                'https://fcm.googleapis.com/v1/projects/test-123456/messages:send',
                body="""{}""",
                content_type="application/json")

            with self.app.test_request_context(
                    '/auth',
                    method='POST',
                    data={
                        "username":
                        "******",
                        "realm":
                        self.realm1,
                        # this will be overwritted by pushtoken_disable_wait
                        PUSH_ACTION.WAIT:
                        "10",
                        "password":
                        "******"
                    }):
                res = self.app.full_dispatch_request()
                self.assertEqual(res.status_code, 401)
                jsonresp = res.json
                self.assertFalse(jsonresp.get("result").get("value"))
                self.assertFalse(jsonresp.get("result").get("status"))
                self.assertEqual(
                    jsonresp.get("detail").get("serial"),
                    tokenobj.token.serial)
                self.assertIn("transaction_id", jsonresp.get("detail"))
                transaction_id = jsonresp.get("detail").get("transaction_id")
                self.assertEqual(
                    jsonresp.get("detail").get("message"),
                    DEFAULT_CHALLENGE_TEXT)

        # Get the challenge from the database
        challengeobject_list = get_challenges(serial=tokenobj.token.serial,
                                              transaction_id=transaction_id)
        challenge = challengeobject_list[0].challenge
        # This is what the smartphone answers.
        # create the signature:
        sign_data = "{0!s}|{1!s}".format(challenge, tokenobj.token.serial)
        signature = b32encode_and_unicode(
            self.smartphone_private_key.sign(sign_data.encode("utf-8"),
                                             padding.PKCS1v15(),
                                             hashes.SHA256()))

        # We still cannot log in
        with self.app.test_request_context('/auth',
                                           method='POST',
                                           data={
                                               "username": "******",
                                               "realm": self.realm1,
                                               "pass": "",
                                               "transaction_id": transaction_id
                                           }):
            res = self.app.full_dispatch_request()
            self.assertEqual(res.status_code, 401)
            self.assertFalse(res.json['result']['status'])

        # Answer the challenge
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={
                                               "serial": tokenobj.token.serial,
                                               "nonce": challenge,
                                               "signature": signature
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertTrue(res.json['result']['value'])

        # We can now log in
        with self.app.test_request_context('/auth',
                                           method='POST',
                                           data={
                                               "username": "******",
                                               "realm": self.realm1,
                                               "pass": "",
                                               "transaction_id": transaction_id
                                           }):
            res = self.app.full_dispatch_request()
            self.assertEqual(res.status_code, 200)
            self.assertTrue(res.json['result']['status'])

        delete_policy("push1")
        delete_policy("webui")