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
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
#include <portable/krb5.h>
#include <portable/system.h>
+#include <ctype.h>
#include <errno.h>
#include <plugin/internal.h>
#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.
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
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;
}
* space.
*/
struct class_rule {
- size_t min;
- size_t max;
+ unsigned long min;
+ unsigned long max;
bool lower;
bool upper;
bool digit;
[
{
- "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"
}
]
* 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>
*/
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;
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;
* 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>
/*
* 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
*/
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");
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";
#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");
}
# 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.