From: Russ Allbery Date: Thu, 12 Dec 2013 06:10:27 +0000 (-0800) Subject: First pass at support for character class rules X-Git-Tag: release/2.2~15 X-Git-Url: https://git.eyrie.org/?a=commitdiff_plain;h=e8bc8dc1b12930898cf2d4abc6fac55b25c73011;p=kerberos%2Fkrb5-strength.git First pass at support for character class rules 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. --- diff --git a/Makefile.am b/Makefile.am index 997ed36..74708c5 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 index 0000000..683b5de --- /dev/null +++ b/plugin/classes.c @@ -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 + * Copyright 2013 + * The Board of Trustees of the Leland Stanford Junior University + * + * See LICENSE for licensing terms. + */ + +#include +#include + +#include + +#include + +/* 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; +} diff --git a/plugin/config.c b/plugin/config.c index d60ef50..70394b5 100644 --- a/plugin/config.c +++ b/plugin/config.c @@ -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 diff --git a/plugin/error.c b/plugin/error.c index 583c723..9fc0422 100644 --- a/plugin/error.c +++ b/plugin/error.c @@ -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) diff --git a/plugin/general.c b/plugin/general.c index 644d76e..4dde3ea 100644 --- a/plugin/general.c +++ b/plugin/general.c @@ -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) diff --git a/plugin/internal.h b/plugin/internal.h index fe13b9f..7431c22 100644 --- a/plugin/internal.h +++ b/plugin/internal.h @@ -20,6 +20,7 @@ #ifdef HAVE_CDB_H # include #endif +#include #ifdef HAVE_KRB5_PWQUAL_PLUGIN_H # include @@ -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 index 0000000..b7895be --- /dev/null +++ b/plugin/vector.c @@ -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 + * Copyright 2013 + * The Board of Trustees of the Leland Stanford Junior University + * + * See LICENSE for licensing terms. + */ + +#include +#include + +#include + + +/* + * 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; +} diff --git a/portable/kadmin.h b/portable/kadmin.h index d4c591c..f410481 100644 --- a/portable/kadmin.h +++ b/portable/kadmin.h @@ -50,6 +50,11 @@ # 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