Repository: privacyidea/privacyidea

Issue:
define characters that can be used in set_random_pin
The policy ``otp_set_random_pin`` can be used to set a random PIN of a specific length.
But the contents, the used characters, of this random PIN can not be configured.

We could also use the policy ``otp_pin_contents``. 


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
@@ -78,7 +78,8 @@
                                   split_user, User)
 from privacyidea.lib.token import (get_tokens, get_realms_of_token, get_token_type)
 from privacyidea.lib.utils import (get_client_ip,
-                                   parse_timedelta, is_true, check_pin_policy, get_module_class,
+                                   parse_timedelta, is_true, generate_charlists_from_pin_policy,
+                                   check_pin_policy, get_module_class,
                                    determine_logged_in_userparams)
 from privacyidea.lib.crypto import generate_password
 from privacyidea.lib.auth import ROLE
@@ -111,7 +112,6 @@
 optional = True
 required = False
 
-
 class prepolicy(object):
     """
     This is the decorator wrapper to call a specific function before an API
@@ -152,6 +152,21 @@ def policy_wrapper(*args, **kwds):
 
         return policy_wrapper
 
+def _generate_pin_from_policy(policy, size=6):
+    """
+    This helper function creates a string of allowed characters from the value of a pincontents policy.
+
+    :param policy: The policy that describes the allowed contents of the PIN (see check_pin_policy).
+    :param size: The desired length of the generated pin
+    :return: The generated PIN
+    """
+
+    charlists_dict = generate_charlists_from_pin_policy(policy)
+
+    pin = generate_password(size=size, characters=charlists_dict['base'],
+                      requirements=charlists_dict['requirements'])
+    return pin
+
 
 def set_random_pin(request=None, action=None):
     """
@@ -182,8 +197,23 @@ def set_random_pin(request=None, action=None):
         raise TokenAdminError("You need to specify a policy '{0!s}' in scope "
                               "{1!s}.".format(ACTION.OTPPINSETRANDOM, role))
     elif len(pin_pols) == 1:
-        log.debug("Creating random OTP PIN with length {0!s}".format(list(pin_pols)[0]))
-        request.all_data["pin"] = generate_password(size=int(list(pin_pols)[0]))
+        # check pin contents policy per token type, otherwise fall back
+        tokentype = get_token_type(request.all_data.get("serial"))
+        pol_contents = Match.admin_or_user(g, action="{0!s}_{1!s}".format(tokentype, ACTION.OTPPINCONTENTS),
+                                           user_obj=request.User).action_values(unique=True)
+        if not pol_contents:
+            pol_contents = Match.admin_or_user(g, action=ACTION.OTPPINCONTENTS,
+                                               user_obj=request.User).action_values(unique=True)
+
+        if len(pol_contents) == 1:
+            log.info("Creating random OTP PIN with length {0!s} "
+                      "matching the contents policy {1!s}".format(list(pin_pols)[0], list(pol_contents)[0]))
+            # generate a pin which matches the contents requirement
+            r = _generate_pin_from_policy(list(pol_contents)[0], size=int(list(pin_pols)[0]))
+            request.all_data["pin"] = r
+        else:
+            log.debug("Creating random OTP PIN with length {0!s}".format(list(pin_pols)[0]))
+            request.all_data["pin"] = generate_password(size=int(list(pin_pols)[0]))
 
     return True
 
@@ -194,8 +224,8 @@ def init_random_pin(request=None, action=None):
     If the policy is set accordingly it adds a random PIN to the
     request.all_data like.
 
-    It uses the policy SCOPE.ENROLL, ACTION.OTPPINRANDOM to set a random OTP
-    PIN during Token enrollment
+    It uses the policy SCOPE.ENROLL, ACTION.OTPPINRANDOM and ACTION.OTPPINCONTENTS
+    to set a random OTP PIN during Token enrollment
     """
     params = request.all_data
     user_object = get_user_from_param(params)
@@ -203,8 +233,23 @@ def init_random_pin(request=None, action=None):
     pin_pols = Match.user(g, scope=SCOPE.ENROLL, action=ACTION.OTPPINRANDOM,
                           user_object=user_object).action_values(unique=True)
     if len(pin_pols) == 1:
-        log.debug("Creating random OTP PIN with length {0!s}".format(list(pin_pols)[0]))
-        request.all_data["pin"] = generate_password(size=int(list(pin_pols)[0]))
+        # check pin contents policy per token type, otherwise fall back
+        tokentype = request.all_data.get("type", "hotp")
+        pol_contents = Match.admin_or_user(g, action="{0!s}_{1!s}".format(tokentype, ACTION.OTPPINCONTENTS),
+                                           user_obj=request.User).action_values(unique=True)
+        if not pol_contents:
+            pol_contents = Match.admin_or_user(g, action=ACTION.OTPPINCONTENTS,
+                                               user_obj=request.User).action_values(unique=True)
+
+        if len(pol_contents) == 1:
+            log.info("Creating random OTP PIN with length {0!s} "
+                      "matching the contents policy {1!s}".format(list(pin_pols)[0], list(pol_contents)[0]))
+            # generate a pin which matches the contents requirement
+            r = _generate_pin_from_policy(list(pol_contents)[0], size=int(list(pin_pols)[0]))
+            request.all_data["pin"] = r
+        else:
+            log.debug("Creating random OTP PIN with length {0!s}".format(list(pin_pols)[0]))
+            request.all_data["pin"] = generate_password(size=int(list(pin_pols)[0]))
 
         # handle the PIN
         handle_pols = Match.user(g, scope=SCOPE.ENROLL, action=ACTION.PINHANDLING,
@@ -1378,7 +1423,7 @@ def indexedsecret_force_attribute(request, action):
 
     return True
 
-  
+
 def webauthntoken_request(request, action):
     """
     This is a WebAuthn token specific wrapper for all endpoints using WebAuthn tokens.
diff --git a/privacyidea/lib/crypto.py b/privacyidea/lib/crypto.py
--- a/privacyidea/lib/crypto.py
+++ b/privacyidea/lib/crypto.py
@@ -811,16 +811,28 @@ def generate_otpkey(key_size=20):
 
 
 def generate_password(size=6, characters=string.ascii_lowercase +
-                        string.ascii_uppercase + string.digits):
+                        string.ascii_uppercase + string.digits, requirements=[]):
     """
     Generate a random password of the specified length of the given characters
+    with optional requirements
 
-    :param size: The length of the password
+    :param size: The length of the password. If smaller than the number of requirements,
+    the requirements define the password length.
     :param characters: The characters the password may consist of
+    :param requirements: A list of strings from which the password must contain at least one character.
+    Note that the requirements are not automatically added to the base character set.
     :return: password
     :rtype: basestring
     """
-    return ''.join(urandom.choice(characters) for _x in range(size))
+    if len(requirements) > size:
+        log.info('The number of requirements is larger then the password length.')
+    # add one random character from each string in the requirements list
+    passwd = [urandom.choice(str) for str in requirements]
+    # fill the password until size with allowed characters
+    passwd.extend(urandom.choice(characters) for _x in range(size - len(requirements)))
+    # return shuffled password
+    random.shuffle(passwd)
+    return "".join(passwd)
 
 
 def generate_keypair(rsa_keysize=2048):
diff --git a/privacyidea/lib/utils/__init__.py b/privacyidea/lib/utils/__init__.py
--- a/privacyidea/lib/utils/__init__.py
+++ b/privacyidea/lib/utils/__init__.py
@@ -58,6 +58,10 @@
 
 ALLOWED_SERIAL = "^[0-9a-zA-Z\-_]+$"
 
+# character lists for the identifiers in the pin content policy
+CHARLIST_CONTENTPOLICY = {"c": string.ascii_letters, # characters
+                          "n": string.digits,        # numbers
+                          "s": string.punctuation}   # special
 
 def check_time_in_range(time_range, check_time=None):
     """
@@ -1135,6 +1139,53 @@ def truncate_comma_list(data, max_len):
     return ",".join(data)
 
 
+def generate_charlists_from_pin_policy(policy):
+    """
+    This function uses the pin content policy string (e.g. "+cns", "[asdf]") to create the character lists
+    for password generation.
+
+    :param policy: The policy that describes the allowed contents of the PIN (see check_pin_policy)
+    :return: Dictionary with keys "base" for the base set of allowed characters and "requirements"
+     which denotes a list of characters from each of which at least one must be contained in the pin.
+    """
+
+    # regexp to check for pin content policy string validity
+    VALID_POLICY_REGEXP = re.compile(r'^[+-]*[cns]+$|^\[.*\]+$')
+
+    # default: full character list
+    base_characters = "".join(CHARLIST_CONTENTPOLICY.values())
+    # list of strings where a character of each string is required for the pin
+    requirements = []
+
+    if not re.match(VALID_POLICY_REGEXP, policy):
+        raise PolicyError("Unknown character specifier in PIN policy.")
+
+    if policy[0] == "+":
+        # grouping
+        for char in policy[1:]:
+            requirements.append(CHARLIST_CONTENTPOLICY.get(char))
+        requirements = ["".join(requirements)]
+
+    elif policy[0] == "-":
+        # exclusion
+        base_charlist = []
+        for key in CHARLIST_CONTENTPOLICY.keys():
+            if key not in policy[1:]:
+                base_charlist.append(CHARLIST_CONTENTPOLICY[key])
+        base_characters = "".join(base_charlist)
+
+    elif policy[0] == "[" and policy[-1] == "]":
+        # only allowed characters
+        base_characters = policy[1:-1]
+
+    else:
+        for c in policy:
+            if c in CHARLIST_CONTENTPOLICY:
+                requirements.append(CHARLIST_CONTENTPOLICY.get(c))
+
+    return {"base": base_characters, "requirements": requirements}
+
+
 def check_pin_policy(pin, policy):
     """
     The policy to check a PIN can contain of "c", "n" and "s".
@@ -1147,52 +1198,27 @@ def check_pin_policy(pin, policy):
     :param policy: The policy that describes the allowed contents of the PIN.
     :return: Tuple of True or False and a description
     """
-    chars = {"c": r"[a-zA-Z]",
-             "n": r"[0-9]",
-             "s": r"[\[\].:,;_<>+*!/()=?$§%&#~^-]"}
+
     ret = True
     comment = []
 
     if not policy:
         return False, "No policy given."
 
-    if policy[0] in ["+", "-"] or policy[0] != "[":
-        for char in policy[1:]:
-            if char not in chars.keys():
-                raise PolicyError("Unknown character specifier in PIN policy.")
+    charlists_dict = generate_charlists_from_pin_policy(policy)
 
-    if policy[0] == "+":
-        # grouping
-        necessary = []
-        for char in policy[1:]:
-            necessary.append(chars.get(char))
-        necessary = "|".join(necessary)
-        if not re.search(necessary, pin):
+    # check for not allowed characters
+    for char in pin:
+        if not char in charlists_dict["base"]:
             ret = False
-            comment.append("Missing character in PIN: {0!s}".format(necessary))
+    if not ret:
+        comment.append("Not allowed character in PIN!")
 
-    elif policy[0] == "-":
-        # exclusion
-        not_allowed = []
-        for char in policy[1:]:
-            not_allowed.append(chars.get(char))
-        not_allowed = "|".join(not_allowed)
-        if re.search(not_allowed, pin):
+    # check requirements
+    for str in charlists_dict["requirements"]:
+        if not re.search(re.compile('[' + str + ']'), pin):
             ret = False
-            comment.append("Not allowed character in PIN!")
-
-    elif policy[0] == "[" and policy[-1] == "]":
-        # only allowed characters
-        allowed_chars = policy[1:-1]
-        for ch in pin:
-            if ch not in allowed_chars:
-                ret = False
-                comment.append("Not allowed character in PIN!")
-    else:
-        for c in chars:
-            if c in policy and not re.search(chars[c], pin):
-                ret = False
-                comment.append("Missing character in PIN: {0!s}".format(chars[c]))
+            comment.append("Missing character in PIN: {0!s}".format(str))
 
     return ret, ",".join(comment)
 


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
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 """
 This test file tests the api.lib.policy.py
 
@@ -27,8 +28,8 @@
                                            check_max_token_user,
                                            check_anonymous_user,
                                            check_max_token_realm, set_realm,
-                                           init_tokenlabel, init_random_pin,
-                                           init_token_defaults,
+                                           init_tokenlabel, init_random_pin, set_random_pin,
+                                           init_token_defaults, _generate_pin_from_policy,
                                            encrypt_pin, check_otp_pin,
                                            enroll_pin,
                                            check_external, api_key_required,
@@ -77,7 +78,7 @@
 from privacyidea.lib.tokenclass import DATE_FORMAT
 from .test_lib_tokens_webauthn import (ALLOWED_TRANSPORTS, CRED_ID, ASSERTION_RESPONSE_TMPL, ASSERTION_CHALLENGE,
                                        RP_ID, RP_NAME, ORIGIN, REGISTRATION_RESPONSE_TMPL)
-from privacyidea.lib.utils import create_img
+from privacyidea.lib.utils import create_img, generate_charlists_from_pin_policy, CHARLIST_CONTENTPOLICY, check_pin_policy
 
 
 HOSTSFILE = "tests/testdata/hosts"
@@ -650,7 +651,7 @@ def test_06_set_tokenlabel(self):
         delete_policy("pol2")
         delete_policy("pol3")
 
-    def test_07_set_random_pin(self):
+    def test_07a_init_random_pin(self):
         g.logged_in_user = {"username": "admin1",
                             "realm": "",
                             "role": "admin"}
@@ -662,11 +663,17 @@ def test_07_set_random_pin(self):
         env["REMOTE_ADDR"] = "10.0.0.1"
         g.client_ip = env["REMOTE_ADDR"]
         req = Request(env)
+        req.User = User("cornelius", self.realm1)
 
-        # Set a policy that defines the tokenlabel
-        set_policy(name="pol1",
+        # Set policies which define the pin generation behavior
+        contents_policy = "+cns"
+        size_policy = 12
+        set_policy(name="pinsize",
                    scope=SCOPE.ENROLL,
-                   action="{0!s}={1!s}".format(ACTION.OTPPINRANDOM, "12"))
+                   action="{0!s}={1!s}".format(ACTION.OTPPINRANDOM, size_policy))
+        set_policy(name="pincontent",
+                   scope=SCOPE.ADMIN,
+                   action="{0!s}={1!s}".format(ACTION.OTPPINCONTENTS, contents_policy))
         set_policy(name="pinhandling",
                    scope=SCOPE.ENROLL,
                    action="{0!s}=privacyidea.lib.pinhandling.base.PinHandler".format(
@@ -676,15 +683,125 @@ def test_07_set_random_pin(self):
         # request, that matches the policy
         req.all_data = {
                         "user": "cornelius",
-                        "realm": "home"}
+                        "realm": "realm1"}
+
         init_random_pin(req)
+        pin = req.all_data.get("pin")
+
+        # check if the pin honors the contents policy
+        pin_valid, comment = check_pin_policy(pin, contents_policy)
+        self.assertTrue(pin_valid)
+
+        # Check, if the pin has the correct length
+        self.assertEqual(len(req.all_data.get("pin")), size_policy)
 
-        # Check, if the tokenlabel was added
-        self.assertEqual(len(req.all_data.get("pin")), 12)
         # finally delete policy
-        delete_policy("pol1")
+        delete_policy("pinsize")
+        delete_policy("pincontent")
         delete_policy("pinhandling")
 
+    def test_07b_set_random_pin(self):
+        g.logged_in_user = {"username": "admin1",
+                            "realm": "",
+                            "role": "admin"}
+        builder = EnvironBuilder(method='POST',
+                                 data={'serial': "OATH123456"},
+                                 headers={})
+        env = builder.get_environ()
+        # Set the remote address so that we can filter for it
+        env["REMOTE_ADDR"] = "10.0.0.1"
+        g.client_ip = env["REMOTE_ADDR"]
+        req = Request(env)
+        req.User = User("cornelius", self.realm1)
+
+        # Set policies which define the pin generation behavior
+        contents_policy = "+cns"
+        size_policy = 12
+        set_policy(name="pinsize",
+                   scope=SCOPE.ADMIN,
+                   action="{0!s}={1!s}".format(ACTION.OTPPINSETRANDOM, size_policy))
+        set_policy(name="pincontent",
+                   scope=SCOPE.ADMIN,
+                   action="{0!s}={1!s}".format(ACTION.OTPPINCONTENTS, contents_policy))
+        g.policy_object = PolicyClass()
+
+        # request, that matches the policy
+        req.all_data = {
+                        "user": "cornelius",
+                        "realm": "realm1"}
+
+        set_random_pin(req)
+        pin = req.all_data.get("pin")
+
+        # check if the pin honors the contents policy
+        pin_valid, comment = check_pin_policy(pin, contents_policy)
+        self.assertTrue(pin_valid)
+
+        # Check, if the pin has the correct length
+        self.assertEqual(len(req.all_data.get("pin")), size_policy)
+
+        # finally delete policy
+        delete_policy("pinsize")
+        delete_policy("pincontent")
+
+    def test_07c_generate_pin_from_policy(self):
+        content_policies_valid = ['+cn', '-s', 'cns', '+ns', '[1234567890]', '[[]€@/(]']
+        content_policies_invalid = ['+c-ns', 'cn-s', '+ns-[1234567890]', '-[1234567890]']
+        pin_size = 3
+        for content_policy in content_policies_valid:
+            pin = _generate_pin_from_policy(content_policy, size=pin_size)
+            # check if the pin honors the contents policy
+            pin_valid, comment = check_pin_policy(pin, content_policy)
+            self.assertTrue(pin_valid)
+            # Check, if the pin has the correct length
+            self.assertEqual(len(pin), pin_size)
+
+        for content_policy in content_policies_invalid:
+            # an invalid policy string should throw a PolicyError exception
+            self.assertRaises(PolicyError, _generate_pin_from_policy, content_policy, size=pin_size)
+
+    def test_07d_generate_charlists_from_pin_policy(self):
+        default_chars = "".join(CHARLIST_CONTENTPOLICY.values())
+
+        policies = ["+cn", "+c", "+cs"]
+        for policy in policies:
+            required = ["".join([CHARLIST_CONTENTPOLICY[str] for str in policy[1:]])]
+            charlists_dict = generate_charlists_from_pin_policy(policy)
+            self.assertEqual(charlists_dict,
+                             {"base": default_chars,
+                              "requirements": required})
+
+        policies = ["-cn", "-c", "-sc"]
+        for policy in policies:
+            base_charlist = []
+            for key in CHARLIST_CONTENTPOLICY.keys():
+                if key not in policy[1:]:
+                    base_charlist.append(CHARLIST_CONTENTPOLICY[key])
+            base_chars = "".join(base_charlist)
+            charlists_dict = generate_charlists_from_pin_policy(policy)
+            self.assertEqual(charlists_dict,
+                             {"base": base_chars,
+                              "requirements": []})
+
+        policies = ["cn", "c", "sc"]
+        for policy in policies:
+            required = [CHARLIST_CONTENTPOLICY[str] for str in policy[:]]
+            charlists_dict = generate_charlists_from_pin_policy(policy)
+            self.assertEqual(charlists_dict,
+                             {"base": default_chars,
+                              "requirements": required})
+
+        policies = ["[cn]", "[1234567890]", "[[]]", "[ÄÖüß§$@³¬&()|<>€%/\]"]
+        for policy in policies:
+            charlists_dict = generate_charlists_from_pin_policy(policy)
+            self.assertEqual(charlists_dict,
+                             {"base": policy[1:-1],
+                              "requirements": []})
+
+        policies = ["+c-n", ".c", ""]
+        for policy in policies:
+            self.assertRaises(PolicyError, generate_charlists_from_pin_policy, policy)
+
     def test_08_encrypt_pin(self):
         g.logged_in_user = {"username": "admin1",
                             "realm": "",
diff --git a/tests/test_lib_crypto.py b/tests/test_lib_crypto.py
--- a/tests/test_lib_crypto.py
+++ b/tests/test_lib_crypto.py
@@ -14,7 +14,8 @@
                                     geturandom, get_alphanum_str, hash_with_pepper,
                                     verify_with_pepper, aes_encrypt_b64, aes_decrypt_b64,
                                     get_hsm, init_hsm, set_hsm_password, hash,
-                                    encrypt, decrypt, Sign, generate_keypair)
+                                    encrypt, decrypt, Sign, generate_keypair,
+                                    generate_password)
 from privacyidea.lib.utils import to_bytes, to_unicode
 from privacyidea.lib.security.default import (SecurityModule,
                                               DefaultSecurityModule)
@@ -23,7 +24,7 @@
 from flask import current_app
 from six import text_type
 from PyKCS11 import PyKCS11Error
-
+import string
 
 class SecurityModuleTestCase(MyTestCase):
     """
@@ -326,6 +327,40 @@ def test_06_hash_pepper(self):
         r = verify_with_pepper(h, "super Password")
         self.assertEqual(r, False)
 
+    def test_07_generate_password(self):
+        # test given default characters
+        pass_numeric = generate_password(size=12, characters=string.digits)
+        self.assertTrue(pass_numeric.isdigit())
+        self.assertEqual(len(pass_numeric), 12)
+
+        # test requirements, we loop to get some statistics
+        default_chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
+        for i in range(10):
+            password_req = generate_password(size=3, characters=default_chars, requirements=["AB", "12"])
+            # a character from each requirement must be found
+            self.assertTrue(
+                any(char in "AB" for char in password_req) and any(char in "12" for char in password_req))
+            self.assertEqual(len(password_req),3)
+
+        # use letters for base and numbers for requirements
+        # this cannot be achieved with a pin policy
+        password = generate_password(size=10, characters=string.ascii_letters,
+                                     requirements=[string.digits, string.digits, string.digits])
+        self.assertEqual(10, len(password))
+        self.assertEqual(3, sum(c.isdigit() for c in password))
+        self.assertEqual(7, sum(c.isalpha() for c in password))
+
+        # requirements define the minimum length of a password
+        password = generate_password(size=0, characters='ABC',
+                                     requirements=['1', '2', '3'])
+        self.assertEqual(3, len(password))
+
+        # empty characters variable raises an IndexError
+        self.assertRaises(IndexError, generate_password, characters='')
+
+        # negative size without requirements results in an empty password
+        password = generate_password(size=-1)
+        self.assertEqual(password, '')
 
 class AESHardwareSecurityModuleTestCase(MyTestCase):
     """
diff --git a/tests/test_lib_utils.py b/tests/test_lib_utils.py
--- a/tests/test_lib_utils.py
+++ b/tests/test_lib_utils.py
@@ -14,7 +14,7 @@
                                    parse_time_offset_from_now, censor_connect_string,
                                    parse_timedelta, to_unicode,
                                    parse_int, convert_column_to_unicode,
-                                   truncate_comma_list, check_pin_policy,
+                                   truncate_comma_list, check_pin_policy, CHARLIST_CONTENTPOLICY,
                                    get_module_class, decode_base32check,
                                    get_client_ip, sanity_name_check, to_utf8,
                                    to_byte_string, hexlify_and_unicode,
@@ -24,6 +24,7 @@
                                    modhex_decode, checksum, urlsafe_b64encode_and_unicode,
                                    check_ip_in_policy, split_pin_pass, create_tag_dict,
                                    check_serial_valid, determine_logged_in_userparams)
+from privacyidea.lib.crypto import generate_password
 from datetime import timedelta, datetime
 from netaddr import IPAddress, IPNetwork, AddrFormatError
 from dateutil.tz import tzlocal, tzoffset, gettz
@@ -570,27 +571,32 @@ def test_20_pin_policy(self):
         r, c = check_pin_policy("1234", "n")
         self.assertTrue(r)
 
-        r, c = check_pin_policy("[[[", "n")
+        r, c = check_pin_policy(r"[[[", "n")
         self.assertFalse(r)
 
-        r, c = check_pin_policy("[[[", "c")
+        r, c = check_pin_policy(r"[[[", "c")
         self.assertFalse(r)
 
-        r, c = check_pin_policy("[[[", "s")
+        r, c = check_pin_policy(r"[[[", "s")
+        self.assertTrue(r)
+
+        # check the validation of a generated password with square brackets
+        password = generate_password(size=3, requirements=['[', '[', '['])
+        r, c = check_pin_policy(password, "s")
         self.assertTrue(r)
 
         r, c = check_pin_policy("abc", "nc")
         self.assertFalse(r)
-        self.assertEqual("Missing character in PIN: [0-9]", c)
+        self.assertEqual("Missing character in PIN: {}".format(CHARLIST_CONTENTPOLICY['n']), c)
 
         r, c = check_pin_policy("123", "nc")
         self.assertFalse(r)
-        self.assertEqual(r"Missing character in PIN: [a-zA-Z]", c)
+        self.assertEqual(r"Missing character in PIN: {}".format(CHARLIST_CONTENTPOLICY['c']), c)
 
         r, c = check_pin_policy("123", "ncs")
         self.assertFalse(r)
-        self.assertTrue(r"Missing character in PIN: [a-zA-Z]" in c, c)
-        self.assertTrue(r"Missing character in PIN: [\[\].:,;_<>+*!/()=?$§%&#~^-]" in c, c)
+        self.assertTrue(r"Missing character in PIN: {}".format(CHARLIST_CONTENTPOLICY['c'] in c), c)
+        self.assertTrue(r"Missing character in PIN: {}".format(CHARLIST_CONTENTPOLICY['s'] in c), c)
 
         r, c = check_pin_policy("1234", "")
         self.assertFalse(r)
@@ -612,7 +618,8 @@ def test_20_pin_policy(self):
 
         r, c = check_pin_policy("@@@@", "+cn")
         self.assertFalse(r)
-        self.assertEqual(c, "Missing character in PIN: [a-zA-Z]|[0-9]")
+        self.assertEqual(c, "Missing character in PIN: {}{}".format(CHARLIST_CONTENTPOLICY['c'],
+                                                                    CHARLIST_CONTENTPOLICY['n']))
 
         # check for exclusion
         # No special character