]> eyrie.org Git - kerberos/krb5-strength.git/commitdiff
First pass at support for character class rules
authorRuss Allbery <eagle@eyrie.org>
Thu, 12 Dec 2013 06:10:27 +0000 (22:10 -0800)
committerRuss Allbery <eagle@eyrie.org>
Thu, 12 Dec 2013 06:10:27 +0000 (22:10 -0800)
This compiles, but it's not tested yet.  It supports a list of
required character classes, but not the restriction to particular
password lengths yet.

Makefile.am
plugin/classes.c [new file with mode: 0644]
plugin/config.c
plugin/error.c
plugin/general.c
plugin/internal.h
plugin/vector.c [new file with mode: 0644]
portable/kadmin.h

index 997ed364b132d238748485948a4735a4ad3ded03..74708c5499ca5577530c3a0b9c554ed04d5020f9 100644 (file)
@@ -55,9 +55,9 @@ moduledir = $(libdir)/krb5/plugins/pwqual
 
 # Rules for building the password strength plugin.
 module_LTLIBRARIES = plugin/strength.la
-plugin_strength_la_SOURCES = plugin/cdb.c plugin/config.c plugin/cracklib.c \
-       plugin/error.c plugin/general.c plugin/heimdal.c plugin/internal.h  \
-       plugin/mit.c plugin/principal.c
+plugin_strength_la_SOURCES = plugin/cdb.c plugin/classes.c plugin/config.c \
+       plugin/cracklib.c plugin/error.c plugin/general.c plugin/heimdal.c \
+       plugin/internal.h plugin/mit.c plugin/principal.c plugin/vector.c
 plugin_strength_la_LDFLAGS = -module -avoid-version
 if EMBEDDED_CRACKLIB
     plugin_strength_la_LIBADD = cracklib/libcracklib.la
@@ -69,9 +69,10 @@ plugin_strength_la_LIBADD += portable/libportable.la $(KRB5_LIBS) $(CDB_LIBS)
 # The Heimdal external check program.
 bin_PROGRAMS = tools/heimdal-strength
 tools_heimdal_strength_CFLAGS = $(AM_CFLAGS)
-tools_heimdal_strength_SOURCES = plugin/cdb.c plugin/config.c              \
-       plugin/cracklib.c plugin/error.c plugin/general.c plugin/internal.h \
-       plugin/principal.c tools/heimdal-strength.c
+tools_heimdal_strength_SOURCES = plugin/cdb.c plugin/classes.c           \
+       plugin/config.c plugin/cracklib.c plugin/error.c plugin/general.c \
+       plugin/internal.h plugin/principal.c plugin/vector.c              \
+       tools/heimdal-strength.c
 if EMBEDDED_CRACKLIB
     tools_heimdal_strength_LDADD = cracklib/libcracklib.la
 else
diff --git a/plugin/classes.c b/plugin/classes.c
new file mode 100644 (file)
index 0000000..683b5de
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * Password strength checks for character classes.
+ *
+ * Checks whether the password satisfies a set of character class rules.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2013
+ *     The Board of Trustees of the Leland Stanford Junior University
+ *
+ * See LICENSE for licensing terms.
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <ctype.h>
+
+#include <plugin/internal.h>
+
+/* Stores the characteristics of a particular password as boolean flags. */
+struct password_classes {
+    bool lower;
+    bool upper;
+    bool digit;
+    bool symbol;
+};
+
+/* Abbreviate the most common error reporting syntax. */
+#define MUST_HAVE(ctx, err) \
+    strength_error_class((ctx), "password must contain" err)
+
+
+/*
+ * Analyze a password and fill out a struct with flags indicating which
+ * character classes are present in the password.
+ */
+static void
+analyze_password(const char *password, struct password_classes *classes)
+{
+    const char *p;
+
+    for (p = password; p != '\0'; p++) {
+        if (islower((unsigned char) *p))
+            classes->lower = true;
+        else if (isupper((unsigned char) *p))
+            classes->upper = true;
+        else if (isdigit((unsigned char) *p))
+            classes->digit = true;
+        else
+            classes->symbol = true;
+    }
+}
+
+
+/*
+ * Check whether a password satisfies a required character class rule, given
+ * the length of the password and the classes.  Returns 0 if it does and a
+ * Kerberos error code if it does not.
+ */
+static krb5_error_code
+check_rule(krb5_context ctx, struct class_rule *rule, size_t length,
+           struct password_classes *classes)
+{
+    if (length < rule->min || (rule->max > 0 && length > rule->max))
+        return 0;
+    if (rule->lower && !classes->lower)
+        return MUST_HAVE(ctx, "a lowercase letter");
+    if (rule->upper && !classes->upper)
+        return MUST_HAVE(ctx, "an uppercase letter");
+    if (rule->digit && !classes->digit)
+        return MUST_HAVE(ctx, "a digit");
+    if (rule->symbol && !classes->symbol)
+        return MUST_HAVE(ctx, "a symbol");
+    return 0;
+}
+
+
+/*
+ * Check whether a password satisfies the configured character class
+ * restrictions.
+ */
+krb5_error_code
+strength_check_classes(krb5_context ctx, krb5_pwqual_moddata data,
+                       const char *password)
+{
+    struct password_classes classes;
+    size_t length;
+    struct class_rule *rule;
+    krb5_error_code code;
+
+    if (data->rules == NULL)
+        return 0;
+    analyze_password(password, &classes);
+    length = strlen(password);
+    for (rule = data->rules; rule != NULL; rule = rule->next) {
+        code = check_rule(ctx, rule, length, &classes);
+        if (code != 0)
+            return code;
+    }
+    return 0;
+}
index d60ef509befcc1a32097f0af7b7c0fa8f1849e27..70394b5093362684de8fabf989e77236ac4c2fe0 100644 (file)
@@ -127,6 +127,143 @@ strength_config_boolean(krb5_context ctx, const char *opt, bool *result)
 }
 
 
+/*
+ * Parse a single class specification.  Currently, this assumes that the class
+ * specification is a comma-separated list of required classes, and those
+ * classes are required for any length of password.  This will be enhanced
+ * later.
+ */
+static krb5_error_code
+parse_class(krb5_context ctx, const char *spec, struct class_rule **rule)
+{
+    struct vector *classes;
+    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");
+
+    /* Create the basic rule structure. */
+    *rule = calloc(1, sizeof(struct class_rule));
+    (*rule)->min = 0;
+    (*rule)->max = 0;
+
+    /*
+     * Walk the list of required classes and set our flags, diagnosing an
+     * unknown character class.
+     */
+    for (i = 0; i < classes->count; i++) {
+        if (strcmp(classes->strings[i], "upper") == 0)
+            (*rule)->upper = true;
+        else if (strcmp(classes->strings[i], "lower") == 0)
+            (*rule)->lower = true;
+        else if (strcmp(classes->strings[i], "digit") == 0)
+            (*rule)->digit = true;
+        else if (strcmp(classes->strings[i], "symbol") == 0)
+            (*rule)->symbol = true;
+        else {
+            code = strength_error_config(ctx, "unknown character class %s",
+                                         classes->strings[i]);
+            strength_vector_free(classes);
+            free(*rule);
+            *rule = NULL;
+            return code;
+        }
+    }
+    strength_vector_free(classes);
+    return 0;
+}
+
+
+/*
+ * Parse character class requirements from Kerberos appdefaults.  Takes the
+ * Kerberos context, the option, and the place to store the linked list of
+ * class requirements.
+ */
+krb5_error_code
+strength_config_classes(krb5_context ctx, const char *opt,
+                        struct class_rule **result)
+{
+    struct vector *config;
+    struct class_rule *rules, *last, *tmp;
+    krb5_error_code code;
+    size_t i;
+
+    /* Get the basic configuration as a list. */
+    code = strength_config_list(ctx, opt, &config);
+    if (code != 0)
+        return code;
+    if (config == NULL || config->count == 0) {
+        *result = NULL;
+        return 0;
+    }
+
+    /* Each word in the list will be a class rule. */
+    code = parse_class(ctx, config->strings[0], &rules);
+    if (code != 0)
+        goto fail;
+    last = rules;
+    for (i = 1; i < config->count; i++) {
+        code = parse_class(ctx, config->strings[i], &last->next);
+        if (code != 0)
+            goto fail;
+        last = last->next;
+    }
+
+    /* Success.  Free the vector and return the results. */
+    strength_vector_free(config);
+    *result = rules;
+    return 0;
+
+fail:
+    last = rules;
+    while (last != NULL) {
+        tmp = last;
+        last = last->next;
+        free(tmp);
+    }
+    strength_vector_free(config);
+    return code;
+}
+
+
+/*
+ * Load a list option from Kerberos appdefaults.  Takes the Kerberos context,
+ * the option, and the result location.  The option is read as a string and
+ * the split on spaces and tabs into a list.
+ *
+ * This requires an annoying workaround because one cannot specify a default
+ * value of NULL with MIT Kerberos, since MIT Kerberos unconditionally calls
+ * strdup on the default value.  There's also no way to determine if memory
+ * allocation failed while parsing or while setting the default value.
+ */
+krb5_error_code
+strength_config_list(krb5_context ctx, const char *opt,
+                     struct vector **result)
+{
+    realm_type realm;
+    char *value = NULL;
+
+    /* Obtain the string from [appdefaults]. */
+    realm = default_realm(ctx);
+    krb5_appdefault_string(ctx, "krb5-sync", realm, opt, "", &value);
+    free_default_realm(ctx, realm);
+
+    /* If we got something back, store it in result. */
+    if (value != NULL) {
+        if (value[0] != '\0') {
+            *result = strength_vector_split_multi(value, " \t", *result);
+            if (*result == NULL)
+                return strength_error_system(ctx, "cannot allocate memory");
+        }
+        krb5_free_string(ctx, value);
+    }
+    return 0;
+}
+
+
 /*
  * Load a number option from Kerberos appdefaults.  Takes the Kerberos
  * context, the option, and the result location.  The native interface doesn't
index 583c7232fe0feda7534b409c3a5fedefa73ebe29..9fc0422d14d2a95a1222526e6ead71bd03b775cc 100644 (file)
@@ -60,6 +60,7 @@ set_error(krb5_context ctx, krb5_error_code code, const char *format,
         return code;                                                    \
     }
 ERROR_FUNC(class,    KADM5_PASS_Q_CLASS)
+ERROR_FUNC(config,   KADM5_MISSING_KRB5_CONF_PARAMS)
 ERROR_FUNC(dict,     KADM5_PASS_Q_DICT)
 ERROR_FUNC(generic,  KADM5_PASS_Q_GENERIC)
 ERROR_FUNC(tooshort, KADM5_PASS_Q_TOOSHORT)
index 644d76e4872a77bbd3ea42bdc59d179d35429eb7..4dde3eae5bfc57e6699270bc2d97aa0ae9d8ed6e 100644 (file)
@@ -139,6 +139,14 @@ strength_check(krb5_context ctx UNUSED, krb5_pwqual_moddata data,
     if (data->nonletter && only_alpha_space(password))
         return strength_error_class(ctx, ERROR_LETTER);
 
+    /*
+     * If desired, check that the password satisfies character class
+     * restrictions.
+     */
+    code = strength_check_classes(ctx, data, password);
+    if (code != 0)
+        return code;
+
     /* Check if the password is based on the principal in some way. */
     code = strength_check_principal(ctx, data, principal, password);
     if (code != 0)
index fe13b9f74b35fc95e411d219078e9e33b2c4921c..7431c2218533be42ee6e9bc23643dcfa8f1ad4d7 100644 (file)
@@ -20,6 +20,7 @@
 #ifdef HAVE_CDB_H
 # include <cdb.h>
 #endif
+#include <stddef.h>
 
 #ifdef HAVE_KRB5_PWQUAL_PLUGIN_H
 # include <krb5/pwqual_plugin.h>
@@ -34,6 +35,30 @@ typedef struct krb5_pwqual_moddata_st *krb5_pwqual_moddata;
 #define ERROR_SHORT    "password is too short"
 #define ERROR_USERNAME "password based on username or principal"
 
+/*
+ * A character class rule, which consists of a minimum length to which the
+ * rule is applied, a maximum length to which the rule is applied, and a set
+ * of flags for which character classes are required.  The symbol class
+ * includes everything that isn't in one of the other classes, including
+ * space.
+ */
+struct class_rule {
+    size_t min;
+    size_t max;
+    bool lower;
+    bool upper;
+    bool digit;
+    bool symbol;
+    struct class_rule *next;
+};
+
+/* Used to store a list of strings, managed by the sync_vector_* functions. */
+struct vector {
+    size_t count;
+    size_t allocated;
+    char **strings;
+};
+
 /*
  * MIT Kerberos uses this type as an abstract data type for any data that a
  * password quality check needs to carry.  Reuse it since then we get type
@@ -43,6 +68,7 @@ struct krb5_pwqual_moddata_st {
     long minimum_length;        /* Minimum password length */
     bool ascii;                 /* Whether to require printable ASCII */
     bool nonletter;             /* Whether to require a non-letter */
+    struct class_rule *rules;   /* Linked list of character class rules */
     char *dictionary;           /* Base path to CrackLib dictionary */
     bool have_cdb;              /* Whether we have a CDB dictionary */
     int cdb_fd;                 /* File descriptor of CDB dictionary */
@@ -99,11 +125,40 @@ krb5_error_code strength_init_cracklib(krb5_context, krb5_pwqual_moddata,
 krb5_error_code strength_check_cracklib(krb5_context, krb5_pwqual_moddata,
                                         const char *password);
 
+/* Check whether the password statisfies character class requirements. */
+krb5_error_code strength_check_classes(krb5_context, krb5_pwqual_moddata,
+                                       const char *password);
+
 /* Check whether the password is based on the principal in some way. */
 krb5_error_code strength_check_principal(krb5_context, krb5_pwqual_moddata,
                                          const char *principal,
                                          const char *password);
 
+/*
+ * Manage vectors, which are counted lists of strings.  The functions that
+ * return a boolean return false if memory allocation fails.
+ */
+struct vector *strength_vector_new(void)
+    __attribute__((__malloc__));
+bool strength_vector_add(struct vector *, const char *string)
+    __attribute__((__nonnull__));
+void strength_vector_free(struct vector *);
+
+/*
+ * vector_split_multi splits on a set of characters.  If the vector argument
+ * is NULL, a new vector is allocated; otherwise, the provided one is reused.
+ * Returns NULL on memory allocation failure, after which the provided vector
+ * may have been modified to only have partial results.
+ *
+ * Empty strings will yield zero-length vectors.  Adjacent delimiters are
+ * treated as a single delimiter by vector_split_multi.  Any leading or
+ * trailing delimiters are ignored, so this function will never create
+ * zero-length strings (similar to the behavior of strtok).
+ */
+struct vector *strength_vector_split_multi(const char *string,
+                                           const char *seps, struct vector *)
+    __attribute__((__nonnull__(1, 2)));
+
 /*
  * Obtain configuration settings from krb5.conf.  These are wrappers around
  * the krb5_appdefault_* APIs that handle setting the section name, obtaining
@@ -112,11 +167,19 @@ krb5_error_code strength_check_principal(krb5_context, krb5_pwqual_moddata,
  */
 void strength_config_boolean(krb5_context, const char *, bool *)
     __attribute__((__nonnull__));
+krb5_error_code strength_config_list(krb5_context, const char *,
+                                     struct vector **)
+    __attribute__((__nonnull__));
 void strength_config_number(krb5_context, const char *, long *)
     __attribute__((__nonnull__));
 void strength_config_string(krb5_context, const char *, char **)
     __attribute__((__nonnull__));
 
+/* Parse the more complex configuration of required character classes. */
+krb5_error_code strength_config_classes(krb5_context, const char *,
+                                        struct class_rule **)
+    __attribute__((__nonnull__));
+
 /*
  * Store a particular password quality error in the Kerberos context.  The
  * _system variant uses errno for the error code and appends the strerror
@@ -124,6 +187,8 @@ void strength_config_string(krb5_context, const char *, char **)
  */
 krb5_error_code strength_error_class(krb5_context, const char *format, ...)
     __attribute__((__nonnull__, __format__(printf, 2, 3)));
+krb5_error_code strength_error_config(krb5_context, const char *format, ...)
+    __attribute__((__nonnull__, __format__(printf, 2, 3)));
 krb5_error_code strength_error_dict(krb5_context, const char *format, ...)
     __attribute__((__nonnull__, __format__(printf, 2, 3)));
 krb5_error_code strength_error_generic(krb5_context, const char *format, ...)
diff --git a/plugin/vector.c b/plugin/vector.c
new file mode 100644 (file)
index 0000000..b7895be
--- /dev/null
@@ -0,0 +1,231 @@
+/*
+ * Vector handling (counted lists of char *'s).
+ *
+ * A vector is a table for handling a list of strings with less overhead than
+ * linked list.  The intention is for vectors, once allocated, to be reused;
+ * this saves on memory allocations once the array of char *'s reaches a
+ * stable size.
+ *
+ * This is based on the from rra-c-util util/vector.c library, but that
+ * library uses xmalloc routines to exit the program if memory allocation
+ * fails.  This is a modified version of the vector library that instead
+ * returns false on failure to allocate memory, allowing the caller to do
+ * appropriate recovery.
+ *
+ * Vectors require list of strings, not arbitrary binary data, and cannot
+ * handle data elements containing nul characters.
+ *
+ * Only the portions of the vector library needed by this module is
+ * implemented.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2013
+ *     The Board of Trustees of the Leland Stanford Junior University
+ *
+ * See LICENSE for licensing terms.
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <plugin/internal.h>
+
+
+/*
+ * Allocate a new, empty vector.  Returns NULL if memory allocation fails.
+ */
+struct vector *
+strength_vector_new(void)
+{
+    return calloc(1, sizeof(struct vector));
+}
+
+
+/*
+ * Resize a vector (using realloc to resize the table).  Return false if
+ * memory allocation fails.
+ */
+static bool
+strength_vector_resize(struct vector *vector, size_t size)
+{
+    size_t i;
+    char **strings;
+
+    /* If we're shrinking the vector, free the excess strings. */
+    if (vector->count > size) {
+        for (i = size; i < vector->count; i++)
+            free(vector->strings[i]);
+        vector->count = size;
+    }
+
+    /* If resizing to zero, free all storage.  Otherwise, realloc. */
+    if (size == 0) {
+        free(vector->strings);
+        vector->strings = NULL;
+    } else {
+        strings = realloc(vector->strings, size * sizeof(char *));
+        if (strings == NULL)
+            return false;
+        vector->strings = strings;
+    }
+    vector->allocated = size;
+    return true;
+}
+
+
+/*
+ * Add a new string to the vector, resizing the vector as necessary.  The
+ * vector is resized an element at a time; if a lot of resizes are expected,
+ * vector_resize should be called explicitly with a more suitable size.
+ * Return false if memory allocation fails.
+ */
+bool
+strength_vector_add(struct vector *vector, const char *string)
+{
+    size_t next = vector->count;
+
+    if (vector->count == vector->allocated)
+        if (!strength_vector_resize(vector, vector->allocated + 1))
+            return false;
+    vector->strings[next] = strdup(string);
+    if (vector->strings[next] == NULL)
+        return false;
+    vector->count++;
+    return true;
+}
+
+
+/*
+ * Empty a vector but keep the allocated memory for the pointer table.
+ */
+static void
+strength_vector_clear(struct vector *vector)
+{
+    size_t i;
+
+    for (i = 0; i < vector->count; i++)
+        if (vector->strings[i] != NULL)
+            free(vector->strings[i]);
+    vector->count = 0;
+}
+
+
+/*
+ * Free a vector completely.
+ */
+void
+strength_vector_free(struct vector *vector)
+{
+    if (vector == NULL)
+        return;
+    strength_vector_clear(vector);
+    free(vector->strings);
+    free(vector);
+}
+
+
+/*
+ * Given a vector that we may be reusing, clear it out.  If the first argument
+ * is NULL, allocate a new vector.  Used by vector_split*.  Returns NULL if
+ * memory allocation fails.
+ */
+static struct vector *
+strength_vector_reuse(struct vector *vector)
+{
+    if (vector == NULL)
+        return strength_vector_new();
+    else {
+        strength_vector_clear(vector);
+        return vector;
+    }
+}
+
+
+/*
+ * Given a string and a set of separators expressed as a string, count the
+ * number of strings that it will split into when splitting on those
+ * separators.
+ */
+static size_t
+split_multi_count(const char *string, const char *seps)
+{
+    const char *p;
+    size_t count;
+
+    /* If the string is empty, the count of components is zero. */
+    if (*string == '\0')
+        return 0;
+
+    /* Otherwise, walk the string looking for non-consecutive separators. */
+    for (count = 1, p = string + 1; *p != '\0'; p++)
+        if (strchr(seps, *p) != NULL && strchr(seps, p[-1]) == NULL)
+            count++;
+
+    /*
+     * If the string ends in separators, we've overestimated the number of
+     * strings by one.
+     */
+    if (strchr(seps, p[-1]) != NULL)
+        count--;
+    return count;
+}
+
+
+/*
+ * Given a string, split it at any of the provided separators to form a
+ * vector, copying each string segment.  If the third argument isn't NULL,
+ * reuse that vector; otherwise, allocate a new one.  Any number of
+ * consecutive separators are considered a single separator.  Returns NULL on
+ * memory allocation failure, after which the provided vector may only have
+ * partial results.
+ */
+struct vector *
+strength_vector_split_multi(const char *string, const char *seps,
+                        struct vector *vector)
+{
+    const char *p, *start;
+    size_t i, count;
+    bool created = false;
+
+    /* Set up the vector we'll use to store the results. */
+    if (vector == NULL)
+        created = true;
+    vector = strength_vector_reuse(vector);
+    if (vector == NULL)
+        return NULL;
+
+    /* Count how big a vector we need and resize accordingly. */
+    count = split_multi_count(string, seps);
+    if (count == 0)
+        return vector;
+    if (vector->allocated < count && !strength_vector_resize(vector, count))
+        goto fail;
+
+    /* Now, walk the string and build the components. */
+    vector->count = 0;
+    for (start = string, p = string, i = 0; *p != '\0'; p++)
+        if (strchr(seps, *p) != NULL) {
+            if (start != p) {
+                vector->strings[i] = strndup(start, (size_t) (p - start));
+                if (vector->strings[i] == NULL)
+                    goto fail;
+                i++;
+                vector->count++;
+            }
+            start = p + 1;
+        }
+
+    /* If there is anything left in the string, we have one more component. */
+    if (start != p) {
+        vector->strings[i] = strndup(start, (size_t) (p - start));
+        if (vector->strings[i] == NULL)
+            goto fail;
+        vector->count++;
+    }
+    return vector;
+
+fail:
+    if (created)
+        strength_vector_free(vector);
+    return NULL;
+}
index d4c591c8ed5036b144c16793bbbcd32bafc0fbc5..f4104814f2d96193e23e9796c8a04cd24ddc041f 100644 (file)
 # define KADM5_PASS_Q_GENERIC KADM5_PASS_Q_DICT
 #endif
 
+/* Heimdal doesn't define KADM5_MISSING_KRB5_CONF_PARAMS. */
+#ifndef KADM5_MISSING_KRB5_CONF_PARAMS
+# define KADM5_MISSING_KRB5_CONF_PARAMS KADM5_MISSING_CONF_PARAMS
+#endif
+
 /*
  * Heimdal provides _ctx functions that take an existing context.  MIT always
  * requires the context be passed in.  Code should use the _ctx variant, and