Repository: privacyidea/privacyidea

Issue:
sslverify for PUSH token enrollment
The push token enrollment should be able to tell the smartphone app if the SSL should be verified or not. 
This is done by adding a parameter ``sslverify=0/1``.
This is also part of the signature.

default is "1", verify SSL.

We need a policy to deactivate this.


Discussion:

Resolving patch:
diff --git a/privacyidea/api/lib/prepolicy.py b/privacyidea/api/lib/prepolicy.py
--- a/privacyidea/api/lib/prepolicy.py
+++ b/privacyidea/api/lib/prepolicy.py
@@ -1254,7 +1254,7 @@ def pushtoken_add_config(request, action):
     This is a token specific wrapper for push token for the endpoint
     /token/init
     According to the policy scope=SCOPE.ENROLL,
-    action=TTL or action=REGISTRATION_URL the parameters are added to the
+    action=SSL_VERIFY or action=FIREBASE_CONFIG the parameters are added to the
     enrollment step.
     :param request:
     :param action:
@@ -1271,7 +1271,7 @@ def pushtoken_add_config(request, action):
             token_resolver = user_object.resolver
         else:
             token_realm = token_resolver = token_user = None
-        # Get the TTL and the Registration URL from the configs
+        # Get the firebase configuration from the policies
         firebase_config = g.policy_object.get_action_values(
             action=PUSH_ACTION.FIREBASE_CONFIG,
             scope=SCOPE.ENROLL,
@@ -1286,6 +1286,21 @@ def pushtoken_add_config(request, action):
         else:
             raise PolicyError("Missing enrollment policy for push token: {0!s}".format(PUSH_ACTION.FIREBASE_CONFIG))
 
+        # Get the sslverify definition from the policies
+        ssl_verify = g.policy_object.get_action_values(
+            action=PUSH_ACTION.SSL_VERIFY,
+            scope=SCOPE.ENROLL,
+            realm=token_realm,
+            user=token_user,
+            resolver=token_resolver,
+            client=g.client_ip,
+            audit_data=g.audit_object.audit_data,
+            unique=True
+        )
+        if len(ssl_verify) == 1:
+            request.all_data[PUSH_ACTION.SSL_VERIFY] = list(ssl_verify)[0]
+        else:
+            request.all_data[PUSH_ACTION.SSL_VERIFY] = "1"
 
 
 def u2ftoken_verify_cert(request, action):
diff --git a/privacyidea/api/lib/utils.py b/privacyidea/api/lib/utils.py
--- a/privacyidea/api/lib/utils.py
+++ b/privacyidea/api/lib/utils.py
@@ -47,7 +47,7 @@
 required = False
 
 
-def getParam(param, key, optional=True, default=None, allow_empty=True):
+def getParam(param, key, optional=True, default=None, allow_empty=True, allowed_values=None):
     """
     returns a parameter from the request parameters.
     
@@ -62,6 +62,8 @@ def getParam(param, key, optional=True, default=None, allow_empty=True):
                     contained in the param.
     :param allow_empty: Set to False is the parameter is a string and is
         not allowed to be empty
+    :param allowed_values: A list of allowed values. If another value is given,
+        then the default value is returned
     :type allow_empty: bool
     
     :return: the value (literal) of the parameter if exists or nothing
@@ -79,6 +81,9 @@ def getParam(param, key, optional=True, default=None, allow_empty=True):
     if not allow_empty and ret == "":
         raise ParameterError("Parameter {0!r} must not be empty".format(key), id=905)
 
+    if allowed_values and ret not in allowed_values:
+            ret = default
+
     return ret
 
 
diff --git a/privacyidea/lib/tokens/pushtoken.py b/privacyidea/lib/tokens/pushtoken.py
--- a/privacyidea/lib/tokens/pushtoken.py
+++ b/privacyidea/lib/tokens/pushtoken.py
@@ -73,6 +73,7 @@ class PUSH_ACTION(object):
     FIREBASE_CONFIG = "push_firebase_configuration"
     MOBILE_TEXT = "push_text_on_mobile"
     MOBILE_TITLE = "push_title_on_mobile"
+    SSL_VERIFY = "push_ssl_verify"
 
 
 def strip_key(key):
@@ -217,6 +218,12 @@ def get_class_info(key=None, ret='all'):
                            'group': "PUSH",
                            'value': [gw.identifier for gw in gws]
                        },
+                       PUSH_ACTION.SSL_VERIFY: {
+                           'type': 'str',
+                           'desc': _('The smartphone needs to verify SSL during the enrollment. (default 1)'),
+                           'group':  "PUSH",
+                           'value': ["0", "1"]
+                       }
                    },
                    SCOPE.AUTH: {
                        PUSH_ACTION.MOBILE_TEXT: {
@@ -228,6 +235,13 @@ def get_class_info(key=None, ret='all'):
                            'type': 'str',
                            'desc': _('The title of the notification, the user sees on his mobile phone.'),
                            'group': 'PUSH'
+                       },
+                       PUSH_ACTION.SSL_VERIFY: {
+                           'type': 'str',
+                           'desc': _('The smartphone needs to verify SSL during authentication. (default 1)'),
+                           'group': "PUSH",
+                           'value': ["0", "1"]
+
                        }
                    }
                },
@@ -309,6 +323,7 @@ def get_init_detail(self, params=None, user=None):
         user = user or User()
         tokenlabel = params.get("tokenlabel", "
")
         tokenissuer = params.get("tokenissuer", "privacyIDEA")
+        sslverify = getParam(params, PUSH_ACTION.SSL_VERIFY, allowed_values=["0", "1"], default="1")
         # Add rollout state the response
         response_detail['rollout_state'] = self.token.rollout_state
 
@@ -326,8 +341,9 @@ def get_init_detail(self, params=None, user=None):
             for k in [FIREBASE_CONFIG.PROJECT_NUMBER, FIREBASE_CONFIG.PROJECT_ID,
                       FIREBASE_CONFIG.APP_ID, FIREBASE_CONFIG.API_KEY]:
                 extra_data[k] = fb_options.get(k)
-            # TODO this allows to upgrade our crypto
+            # this allows to upgrade our crypto
             extra_data["v"] = 1
+            extra_data["sslverify"] = sslverify
             # We display this during the first enrollment step!
             qr_url = create_push_token_url(url=fb_options.get(FIREBASE_CONFIG.REGISTRATION_URL),
                                            user=user.login,
@@ -474,6 +490,11 @@ def create_challenge(self, transactionid=None, options=None):
                                                  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())
@@ -492,10 +513,11 @@ def create_challenge(self, transactionid=None, options=None):
                                "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}".format(**smartphone_data)
+            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())


Test patch:
diff --git a/tests/test_api_lib_policy.py b/tests/test_api_lib_policy.py
--- a/tests/test_api_lib_policy.py
+++ b/tests/test_api_lib_policy.py
@@ -1347,9 +1347,30 @@ def test_22_push_firebase_config(self):
             "type": "push"}
         pushtoken_add_config(req, "init")
         self.assertEqual(req.all_data.get(PUSH_ACTION.FIREBASE_CONFIG), "some-fb-config")
+        self.assertEqual("1", req.all_data.get(PUSH_ACTION.SSL_VERIFY))
+
+        # the request tries to inject a rogue value, but we assure sslverify=1
+        g.policy_object = PolicyClass()
+        req.all_data = {
+            "type": "push",
+            "sslverify": "rogue"}
+        pushtoken_add_config(req, "init")
+        self.assertEqual("1", req.all_data.get(PUSH_ACTION.SSL_VERIFY))
+
+        # set sslverify="0"
+        set_policy(name="push_pol2",
+                   scope=SCOPE.ENROLL,
+                   action="{0!s}=0".format(PUSH_ACTION.SSL_VERIFY))
+        g.policy_object = PolicyClass()
+        req.all_data = {
+            "type": "push"}
+        pushtoken_add_config(req, "init")
+        self.assertEqual(req.all_data.get(PUSH_ACTION.FIREBASE_CONFIG), "some-fb-config")
+        self.assertEqual("0", req.all_data.get(PUSH_ACTION.SSL_VERIFY))
 
         # finally delete policy
         delete_policy("push_pol")
+        delete_policy("push_pol2")
 
 
 class PostPolicyDecoratorTestCase(MyApiTestCase):
diff --git a/tests/test_api_lib_utils.py b/tests/test_api_lib_utils.py
--- a/tests/test_api_lib_utils.py
+++ b/tests/test_api_lib_utils.py
@@ -15,3 +15,13 @@ def test_01_getParam(self):
         self.assertEqual(s, "")
 
         self.assertRaises(ParameterError, getParam, {"serial": ""}, "serial", optional=False, allow_empty=False)
+
+        # check for allowed values
+        v = getParam({"sslverify": "0"}, "sslverify", allowed_values=["0", "1"], default="1")
+        self.assertEqual("0", v)
+
+        v = getParam({"sslverify": "rogue value"}, "sslverify", allowed_values=["0", "1"], default="1")
+        self.assertEqual("1", v)
+
+        v = getParam({}, "sslverify", allowed_values=["0", "1"], default="1")
+        self.assertEqual("1", v)
diff --git a/tests/test_lib_tokens_push.py b/tests/test_lib_tokens_push.py
--- a/tests/test_lib_tokens_push.py
+++ b/tests/test_lib_tokens_push.py
@@ -362,7 +362,7 @@ def check_firebase_params(request):
             # check the signature in the payload!
             data = payload.get("message").get("data")
 
-            sign_string = u"{nonce}|{url}|{serial}|{question}|{title}".format(**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())