]> eyrie.org Git - kerberos/krb5-strength.git/commitdiff
Add class requirement documentation and length ranges
authorRuss Allbery <eagle@eyrie.org>
Fri, 13 Dec 2013 01:08:46 +0000 (17:08 -0800)
committerRuss Allbery <eagle@eyrie.org>
Fri, 13 Dec 2013 01:08:46 +0000 (17:08 -0800)
Add support for qualifying a character class restriction with the
range of lengths of password to which it applies.  Add documentation
and a NEWS entry for the new configuration.

NEWS
README
plugin/config.c
plugin/internal.h
tests/data/passwords/classes.json
tests/plugin/heimdal-t.c
tests/plugin/mit-t.c
tests/tools/heimdal-strength-t

diff --git a/NEWS b/NEWS
index 6bc2272a59504200ded57318cbe70ba7d1162ee9..73e34af6ad1cc671557656b3d1fe33ff33a728c4 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,13 @@
 
 krb5-strength 2.2 (unreleased)
 
+    More complex character class requirements can be specified with the
+    configuration option require_classes.  This option lists the character
+    classes the password must contain.  These restrictions may be
+    qualified with password length ranges, allowing the requirements to
+    change with the length of the password.  See README for more details
+    and the option syntax.
+
     cdbmake-wordlist now supports filtering out words based on maximum
     length (-L) and arbitrary user-provided regular expressions (-x).  It
     also supports running in filter mode to produce a new wordlist instead
diff --git a/README b/README
index d45ca3707b9482eed9d0973969575fc4084b2573..6ea1c8fb06b7e7ad5b34d7404f803b61a09df94d 100644 (file)
--- a/README
+++ b/README
@@ -336,6 +336,45 @@ CONFIGURATION
       interoperability problems with computers with different default
       character sets or Unicode normalization forms.
 
+  require_classes
+
+      This option allows specification of more complex character class
+      requirements.  The value of this parameter should be one or more
+      whitespace-separated rule.  Each rule has the syntax:
+
+          [<min>-<max>:]<class>[,<class>...]
+
+      where <class> is one of "upper", "lower", "digit", or "symbol"
+      (without the quote marks).  The symbol class includes all characters
+      other than alphanumeric characters, including space.  The listed
+      classes must appear in the password.  Separate multiple required
+      classes with a comma (and no space).
+
+      The character class checks will be done in whatever locale the
+      plugin or password check program is run in, which will normally be
+      the C locale but may be different depending on local configuration.
+
+      A simple example:
+
+          require_classes = upper,lower,digit
+
+      This requires all passwords contain at least one uppercase letter,
+      at least one lowercase letter, and at least one digit.
+
+      If present, <min> and <max> specify the minimum password length and
+      maximum password length to which this rule applies.  This allows one
+      to specify character class requirements that change with password
+      length.  So, for example:
+
+          require_classes = 8-19:upper,lower 8-15:digit 8-11:symbol
+
+      requires all passwords from 8 to 11 characters long contain all four
+      character classes, passwords from 12 to 15 characters long contain
+      upper and lower case and a digit, and passwords from 16 to 19
+      characters long contain both upper and lower case.  Passowrds longer
+      than 20 characters have no character class restrictions.  (This
+      example is probably used in conjunction with minimum_length = 8.)
+
   require_non_letter
 
       If set to a true boolean value, the password must contain at least
index 414de048fc6ab268d2bdd6837e7729ca61d3f7cd..33fc07aacde94aeef6530092d8a92bdc22ca4ca2 100644 (file)
@@ -16,6 +16,7 @@
 #include <portable/krb5.h>
 #include <portable/system.h>
 
+#include <ctype.h>
 #include <errno.h>
 
 #include <plugin/internal.h>
@@ -105,6 +106,26 @@ free_default_realm(krb5_context ctx UNUSED, realm_type realm)
 #endif /* !HAVE_KRB5_REALM */
 
 
+/*
+ * Helper function to parse a number.  Takes the string to parse, the unsigned
+ * int in which to store the number, and the pointer to set to the first
+ * invalid character after the number.  Returns true if a number could be
+ * successfully parsed and false otherwise.
+ */
+static bool
+parse_number(const char *string, unsigned long *result, const char **end)
+{
+    unsigned long value;
+
+    errno = 0;
+    value = strtoul(string, (char **) end, 10);
+    if (errno != 0 || *end == string)
+        return false;
+    *result = value;
+    return true;
+}
+
+
 /*
  * Load a boolean option from Kerberos appdefaults.  Takes the Kerberos
  * context, the option, and the result location.
@@ -136,19 +157,42 @@ strength_config_boolean(krb5_context ctx, const char *opt, bool *result)
 static krb5_error_code
 parse_class(krb5_context ctx, const char *spec, struct class_rule **rule)
 {
-    struct vector *classes;
+    struct vector *classes = NULL;
     size_t i;
     krb5_error_code code;
-
-    /* Parse the required classes into a vector. */
-    classes = strength_vector_split_multi(spec, ",", NULL);
-    if (classes == NULL)
-        return strength_error_system(ctx, "cannot allocate memory");
+    const char *end;
+    bool okay;
 
     /* Create the basic rule structure. */
     *rule = calloc(1, sizeof(struct class_rule));
-    (*rule)->min = 0;
-    (*rule)->max = 0;
+
+    /*
+     * If the rule starts with a digit, it starts with a range of affected
+     * password lengths.  Parse that range.
+     */
+    if (isdigit((unsigned char) *spec)) {
+        okay = parse_number(spec, &(*rule)->min, &end);
+        if (okay)
+            okay = (*end == '-');
+        if (okay)
+            okay = parse_number(end + 1, &(*rule)->max, &end);
+        if (okay)
+            okay = (*end == ':');
+        if (okay)
+            spec = end + 1;
+        else {
+            code = strength_error_config(ctx, "bad character class requirement"
+                                         " in configuration: %s", spec);
+            goto fail;
+        }
+    }
+
+    /* Parse the required classes into a vector. */
+    classes = strength_vector_split_multi(spec, ",", NULL);
+    if (classes == NULL) {
+        code = strength_error_system(ctx, "cannot allocate memory");
+        goto fail;
+    }
 
     /*
      * Walk the list of required classes and set our flags, diagnosing an
@@ -166,14 +210,17 @@ parse_class(krb5_context ctx, const char *spec, struct class_rule **rule)
         else {
             code = strength_error_config(ctx, "unknown character class %s",
                                          classes->strings[i]);
-            strength_vector_free(classes);
-            free(*rule);
-            *rule = NULL;
-            return code;
+            goto fail;
         }
     }
     strength_vector_free(classes);
     return 0;
+
+fail:
+    strength_vector_free(classes);
+    free(*rule);
+    *rule = NULL;
+    return code;
 }
 
 
index 7431c2218533be42ee6e9bc23643dcfa8f1ad4d7..7eebf67ae42cf2f3c54498f6ea4c923b77c02b34 100644 (file)
@@ -43,8 +43,8 @@ typedef struct krb5_pwqual_moddata_st *krb5_pwqual_moddata;
  * space.
  */
 struct class_rule {
-    size_t min;
-    size_t max;
+    unsigned long min;
+    unsigned long max;
     bool lower;
     bool upper;
     bool digit;
index b8dfbffea118c0e450565c45bb4437bf72aaccd1..bb0da1a4649a034721184e4accf9be5d6b8033a2 100644 (file)
 [
     {
-        "name": "no lowercase",
+        "name": "no lowercase (11)",
         "principal": "test@EXAMPLE.ORG",
         "password": "PASSWORD98!",
         "code": "KADM5_PASS_Q_CLASS",
         "error": "password must contain a lowercase letter"
     },
     {
-        "name": "no uppercase",
+        "name": "no uppercase (11)",
         "principal": "test@EXAMPLE.ORG",
         "password": "password98!",
         "code": "KADM5_PASS_Q_CLASS",
         "error": "password must contain an uppercase letter"
     },
     {
-        "name": "no digit",
+        "name": "no digit (11)",
         "principal": "test@EXAMPLE.ORG",
         "password": "passwordXX!",
         "code": "KADM5_PASS_Q_CLASS",
         "error": "password must contain a number"
     },
     {
-        "name": "no symbol",
+        "name": "no symbol (11)",
         "principal": "test@EXAMPLE.ORG",
         "password": "passwordXX9",
         "code": "KADM5_PASS_Q_CLASS",
         "error": "password must contain a space or punctuation character"
     },
     {
-        "name": "all classes",
+        "name": "all classes (11)",
         "principal": "test@EXAMPLE.ORG",
         "password": "passwordX9!"
     },
     {
-        "name": "all classes with space",
+        "name": "all classes with space (11)",
         "principal": "test@EXAMPLE.ORG",
         "password": "pass wordX9"
+    },
+    {
+        "name": "no lowercase (15)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "PASSWORD98!WORD",
+        "code": "KADM5_PASS_Q_CLASS",
+        "error": "password must contain a lowercase letter"
+    },
+    {
+        "name": "no uppercase (15)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "password98!word",
+        "code": "KADM5_PASS_Q_CLASS",
+        "error": "password must contain an uppercase letter"
+    },
+    {
+        "name": "no digit (15)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "passwordXX!word",
+        "code": "KADM5_PASS_Q_CLASS",
+        "error": "password must contain a number"
+    },
+    {
+        "name": "no symbol (12)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "passwordXX9w"
+    },
+    {
+        "name": "no symbol (15)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "passwordXX9word"
+    },
+    {
+        "name": "all classes (15)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "passwordX9!word"
+    },
+    {
+        "name": "all classes with space (15)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "pass wordX9word"
+    },
+    {
+        "name": "no lowercase (19)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "PASSWORD98!WORDWORD",
+        "code": "KADM5_PASS_Q_CLASS",
+        "error": "password must contain a lowercase letter"
+    },
+    {
+        "name": "no uppercase (19)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "password98!wordword",
+        "code": "KADM5_PASS_Q_CLASS",
+        "error": "password must contain an uppercase letter"
+    },
+    {
+        "name": "no digit (16)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "passwordXX!wordw"
+    },
+    {
+        "name": "no digit (19)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "passwordXX!wordword"
+    },
+    {
+        "name": "no symbol (19)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "passwordXX9wordword"
+    },
+    {
+        "name": "all classes (19)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "passwordX9!wordword"
+    },
+    {
+        "name": "all classes with space (19)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "pass wordX9wordword"
+    },
+    {
+        "name": "no lowercase (20)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "PASSWORD98!WORDWORDW"
+    },
+    {
+        "name": "no uppercase (20)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "password98!wordwordw"
+    },
+    {
+        "name": "no digit (20)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "passwordXX!wordwordw"
+    },
+    {
+        "name": "no symbol (20)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "passwordXX9wordwordw"
+    },
+    {
+        "name": "all classes (20)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "passwordX9!wordwordw"
+    },
+    {
+        "name": "all classes with space (20)",
+        "principal": "test@EXAMPLE.ORG",
+        "password": "pass wordX9wordwordw"
     }
 ]
index 95deac9aeb4ffe36c1d0c946f1cac746e27b7f53..8638176b2342448c1f01bcd9aee1df9299c0f514 100644 (file)
@@ -29,6 +29,7 @@
  * named cdb_tests, cracklib_tests, and principal_tests.
  */
 #include <tests/data/passwords/cdb.c>
+#include <tests/data/passwords/classes.c>
 #include <tests/data/passwords/cracklib.c>
 #include <tests/data/passwords/length.c>
 #include <tests/data/passwords/letter.c>
@@ -148,6 +149,7 @@ main(void)
      */
     count = ARRAY_SIZE(cracklib_tests);
     count += ARRAY_SIZE(cdb_tests);
+    count += ARRAY_SIZE(classes_tests);
     count += ARRAY_SIZE(length_tests);
     count += ARRAY_SIZE(letter_tests);
     count += ARRAY_SIZE(principal_tests) * 2;
@@ -198,11 +200,21 @@ main(void)
     for (i = 0; i < ARRAY_SIZE(letter_tests); i++)
         is_password_test(verifier, &letter_tests[i]);
 
+    /* Add complex character class restrictions and remove the dictionary. */
+    free(setup_argv[4]);
+    setup_argv[3] = (char *) "require_classes";
+    setup_argv[4] = (char *) "8-19:lower,upper 8-15:digit 8-11:symbol";
+    setup_argv[5] = NULL;
+    run_setup((const char **) setup_argv);
+
+    /* Run the simple character class tests. */
+    for (i = 0; i < ARRAY_SIZE(classes_tests); i++)
+        is_password_test(verifier, &classes_tests[i]);
+
     /*
-     * Add length restrictions and remove the dictionary.  This should only do
-     * length checks without any dictionary checks.
+     * Add length restrictions.  This should only do length checks without any
+     * dictionary checks.
      */
-    free(setup_argv[4]);
     setup_argv[3] = (char *) "minimum_length";
     setup_argv[4] = (char *) "12";
     setup_argv[5] = NULL;
index 7e5a268a71b3533430c1b9e290ef11aa566650ef..06c6f46c132f901fa863da83e80f5b0553fc7a9b 100644 (file)
@@ -30,6 +30,7 @@
  * named cdb_tests, cracklib_tests, and principal_tests.
  */
 #include <tests/data/passwords/cdb.c>
+#include <tests/data/passwords/classes.c>
 #include <tests/data/passwords/cracklib.c>
 #include <tests/data/passwords/length.c>
 #include <tests/data/passwords/letter.c>
@@ -157,7 +158,7 @@ main(void)
 
     /*
      * Calculate how many tests we have.  There are two tests for the module
-     * metadata, five more tests for initializing the plugin, and two tests per
+     * metadata, six more tests for initializing the plugin, and two tests per
      * password test.
      *
      * We run all the CrackLib tests twice, once with an explicit dictionary
@@ -166,10 +167,11 @@ main(void)
      */
     count = 2 * ARRAY_SIZE(cracklib_tests);
     count += ARRAY_SIZE(cdb_tests);
+    count += ARRAY_SIZE(classes_tests);
     count += ARRAY_SIZE(length_tests);
     count += ARRAY_SIZE(letter_tests);
     count += 2 * ARRAY_SIZE(principal_tests);
-    plan(2 + 5 + count * 2);
+    plan(2 + 6 + count * 2);
 
     /* Start with the krb5.conf that contains no dictionary configuration. */
     path = test_file_path("data/krb5.conf");
@@ -263,8 +265,32 @@ main(void)
     vtable->close(ctx, data);
 
     /*
-     * Add length restrictions and remove the dictionary.  This should only do
-     * length checks without any dictionary checks.
+     * Add complex character class configuration to krb5.conf but drop
+     * the dictionary configuration.
+     */
+    setup_argv[3] = (char *) "require_classes";
+    setup_argv[4] = (char *) "8-19:lower,upper 8-15:digit 8-11:symbol";
+    setup_argv[5] = NULL;
+    run_setup((const char **) setup_argv);
+
+    /* Obtain a new Kerberos context with that krb5.conf file. */
+    krb5_free_context(ctx);
+    code = krb5_init_context(&ctx);
+    if (code != 0)
+        bail_krb5(ctx, code, "cannot initialize Kerberos context");
+
+    /* Run all the complex character class tests. */
+    code = vtable->open(ctx, NULL, &data);
+    is_int(0, code, "Plugin initialization (complex character class)");
+    if (code != 0)
+        bail("cannot continue after plugin initialization failure");
+    for (i = 0; i < ARRAY_SIZE(classes_tests); i++)
+        is_password_test(ctx, vtable, data, &classes_tests[i]);
+    vtable->close(ctx, data);
+
+    /*
+     * Add length restrictions.  This should only do length checks without any
+     * dictionary checks.
      */
     setup_argv[3] = (char *) "minimum_length";
     setup_argv[4] = (char *) "12";
@@ -289,7 +315,7 @@ main(void)
 #ifdef HAVE_CDB
 
     /* If built with CDB, set up krb5.conf to use a CDB dictionary instead. */
-    free(dictionary);
+    test_file_path_free(dictionary);
     dictionary = test_file_path("data/wordlist.cdb");
     if (dictionary == NULL)
         bail("cannot find data/wordlist.cdb in the test suite");
index 769f5fc54c9e00dbe9ea159cfd41712dc10d6dc8..eedb640f60e78d3b3d8ab3eb573a6092206046fc 100755 (executable)
@@ -204,8 +204,8 @@ for my $test (@{ $tests{letter} }) {
 }
 
 # Install the krb5.conf file for complex character class restrictions.
-$krb5_conf
-  = create_krb5_conf({ require_classes => 'lower,upper,digit,symbol' });
+my $classes = '8-19:lower,upper 8-15:digit 8-11:symbol';
+$krb5_conf = create_krb5_conf({ require_classes => $classes });
 local $ENV{KRB5_CONFIG} = $krb5_conf;
 
 # Run the complex character class tests.