]> eyrie.org Git - kerberos/krb5-strength.git/commitdiff
Support building without CrackLib support
authorRuss Allbery <eagle@eyrie.org>
Mon, 22 May 2017 02:35:52 +0000 (19:35 -0700)
committerRuss Allbery <eagle@eyrie.org>
Mon, 22 May 2017 02:36:49 +0000 (19:36 -0700)
Support building without CrackLib support by passing
--without-cracklib to configure.  This makes the code a bit simpler
and lighter if you don't intend to ever use the CrackLib support.

NEWS
README
README.md
docs/metadata/build/middle
m4/cracklib.m4
plugin/cracklib.c
plugin/internal.h
plugin/sqlite.c
tests/plugin/heimdal-t.c
tests/plugin/mit-t.c
tests/tools/heimdal-strength-t

diff --git a/NEWS b/NEWS
index 27ec12c20fa5fb8e24da8303bc322e55591cbb54..b72439f74899b07e90399683131c2e4ad4b29cb4 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,11 @@
                     User-Visible krb5-strength Changes
 
+krb5-strength 3.2 (unreleased)
+
+    Support building without CrackLib support by passing
+    --without-cracklib to configure.  This makes the code a bit simpler
+    and lighter if you don't intend to ever use the CrackLib support.
+
 krb5-strength 3.1 (2016-12-25)
 
     A new configuration option, cracklib_maxlen, can be set to skip
diff --git a/README b/README
index 205eb48b390db45a380e41857a9bab66af02d932..8832acdb4f38878b533ec9daa67baf8b4afae90f 100644 (file)
--- a/README
+++ b/README
@@ -188,7 +188,9 @@ BUILDING AND INSTALLATION
   with the system version of CrackLib, pass --with-cracklib to configure.
   You can optionally add a directory, giving the root directory where
   CrackLib was installed, or separately set the include and library path
-  with --with-cracklib-include and --with-cracklib-lib.
+  with --with-cracklib-include and --with-cracklib-lib.  You can also
+  build without any CrackLib support by passing --without-cracklib to
+  configure.
 
   krb5-strength will automatically build with TinyCDB if it is found.  To
   specify the installation path of TinyCDB, use --with-tinycdb.  You can
index 3a7f6a4e4cdf036906e2a2e3d34ad14252780fb1..1e1de91b083f87bcc8fcc795687b56d2aeb0efb3 100644 (file)
--- a/README.md
+++ b/README.md
@@ -182,13 +182,15 @@ By default, the Heimdal external password check function is installed as
 `/usr/local/bin/heimdal-strength`, and the plugin is installed as
 `/usr/local/lib/krb5/plugins/pwqual/strength.so`.  You can change these
 paths with the `--prefix`, `--libdir`, and `--bindir` options to
-configure.
+`configure`.
 
 By default, the embedded version of CrackLib will be used.  To build with
-the system version of CrackLib, pass `--with-cracklib` to configure.  You
-can optionally add a directory, giving the root directory where CrackLib
-was installed, or separately set the include and library path with
-`--with-cracklib-include` and `--with-cracklib-lib`.
+the system version of CrackLib, pass `--with-cracklib` to `configure`.
+You can optionally add a directory, giving the root directory where
+CrackLib was installed, or separately set the include and library path
+with `--with-cracklib-include` and `--with-cracklib-lib`.  You can also
+build without any CrackLib support by passing `--without-cracklib` to
+`configure`.
 
 krb5-strength will automatically build with TinyCDB if it is found.  To
 specify the installation path of TinyCDB, use `--with-tinycdb`.  You can
index 38afd2a506fd17d5ae6229340a40b64ee5bab96c..2081c59873237903f2b826965b11a277f00b3a30 100644 (file)
@@ -2,13 +2,15 @@ By default, the Heimdal external password check function is installed as
 `/usr/local/bin/heimdal-strength`, and the plugin is installed as
 `/usr/local/lib/krb5/plugins/pwqual/strength.so`.  You can change these
 paths with the `--prefix`, `--libdir`, and `--bindir` options to
-configure.
+`configure`.
 
 By default, the embedded version of CrackLib will be used.  To build with
-the system version of CrackLib, pass `--with-cracklib` to configure.  You
-can optionally add a directory, giving the root directory where CrackLib
-was installed, or separately set the include and library path with
-`--with-cracklib-include` and `--with-cracklib-lib`.
+the system version of CrackLib, pass `--with-cracklib` to `configure`.
+You can optionally add a directory, giving the root directory where
+CrackLib was installed, or separately set the include and library path
+with `--with-cracklib-include` and `--with-cracklib-lib`.  You can also
+build without any CrackLib support by passing `--without-cracklib` to
+`configure`.
 
 krb5-strength will automatically build with TinyCDB if it is found.  To
 specify the installation path of TinyCDB, use `--with-tinycdb`.  You can
index d19bd1533826d248d969a545ca8ea3566fbfa1ba..7dcd25393d2555c413cb376664c57ab0aa1d80f4 100644 (file)
@@ -67,6 +67,7 @@ AC_DEFUN([_RRA_LIB_CRACKLIB_CHECK],
 dnl The main macro.
 AC_DEFUN([RRA_LIB_CRACKLIB],
 [rra_system_cracklib=
+ rra_no_cracklib=
  rra_cracklib_root=
  rra_cracklib_libdir=
  rra_cracklib_includedir=
@@ -81,6 +82,7 @@ AC_DEFUN([RRA_LIB_CRACKLIB],
     [AS_HELP_STRING([--with-cracklib@<:@=DIR@:>@],
         [Use system CrackLib instead of embedded copy])],
     [AS_IF([test x"$withval" != xno], [rra_system_cracklib=yes])
+     AS_IF([test x"$withval" == xno], [rra_no_cracklib=yes])
      AS_IF([test x"$withval" != xyes && test x"$withval" != xno],
         [rra_cracklib_root="$withval"])])
  AC_ARG_WITH([cracklib-include],
@@ -94,7 +96,10 @@ AC_DEFUN([RRA_LIB_CRACKLIB],
     [AS_IF([test x"$withval" != xyes && test x"$withval" != xno],
         [rra_cracklib_libdir="$withval"])])
 
- AM_CONDITIONAL([EMBEDDED_CRACKLIB], [test x"$rra_system_cracklib" != xyes])
+ AS_IF([test x"$rra_no_cracklib" != xyes],
+     [AC_DEFINE([HAVE_CRACKLIB], 1, [Define if CrackLib is available.])])
+ AM_CONDITIONAL([EMBEDDED_CRACKLIB],
+     [test x"$rra_system_cracklib" != xyes && test x"$rra_no_cracklib" != xyes])
  AS_IF([test x"$rra_system_cracklib" = xyes],
      [_RRA_LIB_CRACKLIB_PATHS
       CRACKLIB_LIBS="-lcrack"
index 7bb8c7e863a8fad6164c91702d05be685eb40c7c..8efa27913475eac34723f667dc5848fe36aeb3ef 100644 (file)
@@ -8,6 +8,7 @@
  * Developed by Derrick Brashear and Ken Hornstein of Sine Nomine Associates,
  *     on behalf of Stanford University
  * Extensive modifications by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017 Russ Allbery <eagle@eyrie.org>
  * Copyright 2006, 2007, 2009, 2012, 2013
  *     The Board of Trustees of the Leland Stanford Junior University
  *
  */
 
 #include <config.h>
+#include <portable/kadmin.h>
 #include <portable/system.h>
 
 #include <plugin/internal.h>
+#include <util/macros.h>
 
 /* When using the embedded CrackLib, we need to provide our own prototype. */
-#ifdef HAVE_CRACK_H
-# include <crack.h>
-#else
+#ifdef HAVE_CRACKLIB
+# ifdef HAVE_CRACK_H
+#  include <crack.h>
+# else
 extern const char *FascistCheck(const char *password, const char *dict);
+# endif
 #endif
 
 
+/*
+ * Stub for strength_init_cracklib if not built with CrackLib support.
+ */
+#ifndef HAVE_CRACKLIB
+krb5_error_code
+strength_init_cracklib(krb5_context ctx, krb5_pwqual_moddata data UNUSED,
+                       const char *dictionary UNUSED)
+{
+    char *path = NULL;
+
+    /* Get CDB dictionary path from krb5.conf. */
+    strength_config_string(ctx, "password_dictionary", &path);
+
+    /* If it was set, report an error, since we don't have CrackLib support. */
+    if (path == NULL)
+        return 0;
+    free(path);
+    krb5_set_error_message(ctx, KADM5_BAD_SERVER_PARAMS, "CrackLib dictionary"
+                           " requested but not built with CrackLib support");
+    return KADM5_BAD_SERVER_PARAMS;
+}
+#endif
+
+
+/* Skip the rest of this file if CrackLib is not available. */
+#ifdef HAVE_CRACKLIB
+
 /*
  * Initialize the CrackLib dictionary.  Ensure that the dictionary file exists
  * and is readable and store the path in the module context.  Returns 0 on
@@ -100,3 +132,5 @@ strength_check_cracklib(krb5_context ctx, krb5_pwqual_moddata data,
     else
         return 0;
 }
+
+#endif /* HAVE_CRACKLIB */
index 011bbb6cd6a31aff6f20d0f29964d43d54d30678..eb58c6530b2d89d377619ad80e3b3309a1fdafce 100644 (file)
@@ -139,11 +139,19 @@ void strength_close_cdb(krb5_context, krb5_pwqual_moddata);
  * CrackLib handling.  strength_init_cracklib gets the dictionary
  * configuration does some sanity checks on it, and strength_check_cracklib
  * checks the password against CrackLib.
+ *
+ * If not built with CrackLib support, provide a stub for check.  init is
+ * always a real function, which reports an error if CrackLib is requested and
+ * not availble.
  */
 krb5_error_code strength_init_cracklib(krb5_context, krb5_pwqual_moddata,
                                        const char *dictionary);
+#ifdef HAVE_CRACKLIB
 krb5_error_code strength_check_cracklib(krb5_context, krb5_pwqual_moddata,
                                         const char *password);
+#else
+# define strength_check_cracklib(c, d, p) 0
+#endif
 
 /*
  * SQLite handling.  strength_init_sqlite gets the database configuration and
index 30bc9a5ab7b8a349c672717a4c167b627b41a6ef..92ceb5f82ac688dcb913a515eb15bb0540b12c7a 100644 (file)
@@ -72,7 +72,7 @@ strength_init_sqlite(krb5_context ctx, krb5_pwqual_moddata data UNUSED)
     /* Get CDB dictionary path from krb5.conf. */
     strength_config_string(ctx, "password_dictionary_sqlite", &path);
 
-    /* If it was set, report an error, since we don't have CDB support. */
+    /* If it was set, report an error, since we don't have SQLite support. */
     if (path == NULL)
         return 0;
     free(path);
index 1ec61f436905a692688638b33ce7f94b5e379494..d5ef80a82f0bcd1139e4e2c2eb47971afe790a44 100644 (file)
@@ -168,16 +168,14 @@ main(void)
     /* Load the plugin. */
     verifier = load_plugin(&handle);
 
-    /* Set up our krb5.conf with the dictionary configuration. */
+    /* Set up our krb5.conf with a basic configuration. */
     setup_argv[0] = test_file_path("data/make-krb5-conf");
     if (setup_argv[0] == NULL)
         bail("cannot find data/make-krb5-conf in the test suite");
     tmpdir = test_tmpdir();
     setup_argv[1] = path;
     setup_argv[2] = tmpdir;
-    setup_argv[3] = (char *) "password_dictionary";
-    basprintf(&setup_argv[4], "%s/data/dictionary", getenv("BUILD"));
-    setup_argv[5] = NULL;
+    setup_argv[3] = NULL;
     run_setup((const char **) setup_argv);
 
     /* Point KRB5_CONFIG at the newly-generated krb5.conf file. */
@@ -185,11 +183,21 @@ main(void)
     putenv(krb5_config);
     free(krb5_config_empty);
 
+    /* Run principal tests. */
+    for (i = 0; i < ARRAY_SIZE(principal_tests); i++)
+        is_password_test(verifier, &principal_tests[i]);
+
+#ifdef HAVE_CRACKLIB
+
+    /* Add CrackLib tests. */
+    setup_argv[3] = (char *) "password_dictionary";
+    basprintf(&setup_argv[4], "%s/data/dictionary", getenv("BUILD"));
+    setup_argv[5] = NULL;
+    run_setup((const char **) setup_argv);
+
     /* Now, run all of the tests. */
     for (i = 0; i < ARRAY_SIZE(cracklib_tests); i++)
         is_password_test(verifier, &cracklib_tests[i]);
-    for (i = 0; i < ARRAY_SIZE(principal_tests); i++)
-        is_password_test(verifier, &principal_tests[i]);
 
     /*
      * Add length restrictions and a maximum length for CrackLib.  This should
@@ -207,14 +215,25 @@ main(void)
     for (i = 0; i < ARRAY_SIZE(length_tests); i++)
         is_password_test(verifier, &length_tests[i]);
 
+    /* Free the memory allocated for the CrackLib test. */
+    free(setup_argv[4]);
+
+#else
+
+    /* Otherwise, mark the CrackLib tests as skipped. */
+    count = ARRAY_SIZE(cracklib_tests) + ARRAY_SIZE(length_tests);
+    skip_block(count * 2, "not built with CDB support");
+
+#endif /* !HAVE_CRACKLIB */
+
     /* Add simple character class restrictions. */
-    setup_argv[5] = (char *) "minimum_different";
-    setup_argv[6] = (char *) "8";
-    setup_argv[7] = (char *) "require_ascii_printable";
+    setup_argv[3] = (char *) "minimum_different";
+    setup_argv[4] = (char *) "8";
+    setup_argv[5] = (char *) "require_ascii_printable";
+    setup_argv[6] = (char *) "true";
+    setup_argv[7] = (char *) "require_non_letter";
     setup_argv[8] = (char *) "true";
-    setup_argv[9] = (char *) "require_non_letter";
-    setup_argv[10] = (char *) "true";
-    setup_argv[11] = NULL;
+    setup_argv[9] = NULL;
     run_setup((const char **) setup_argv);
 
     /* Run the simple character class tests. */
@@ -222,7 +241,6 @@ main(void)
         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 24-24:3";
     setup_argv[5] = NULL;
index 7eba594156091f927048869c99de21d5d3b74dd1..e192e23232aef3ce7d13fe653ca0bfe16a650f86 100644 (file)
@@ -2,6 +2,7 @@
  * Test for the MIT Kerberos shared module API.
  *
  * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017 Russ Allbery <eagle@eyrie.org>
  * Copyright 2010, 2013, 2014
  *     The Board of Trustees of the Leland Stanford Junior University
  *
@@ -201,34 +202,46 @@ main(void)
     if (code != 0)
         bail("cannot continue after plugin initialization failure");
 
-    /* Now, run all of the tests, with principal tests. */
-    for (i = 0; i < ARRAY_SIZE(cracklib_tests); i++)
-        is_password_test(ctx, vtable, data, &cracklib_tests[i]);
+    /* Run the principal tests. */
     for (i = 0; i < ARRAY_SIZE(principal_tests); i++)
         is_password_test(ctx, vtable, data, &principal_tests[i]);
 
+    /* Run the CrackLib tests if CrackLib is available, otherwise skip them. */
+#ifdef HAVE_CRACKLIB
+    for (i = 0; i < ARRAY_SIZE(cracklib_tests); i++)
+        is_password_test(ctx, vtable, data, &cracklib_tests[i]);
+#else
+    count = ARRAY_SIZE(cracklib_tests);
+    skip_block(count * 2, "not built with CrackLib support");
+#endif
+
     /* Close that initialization of the plugin and destroy that context. */
     vtable->close(ctx, data);
     krb5_free_context(ctx);
     ctx = NULL;
 
-    /* Set up our krb5.conf with the dictionary configuration. */
+    /* Set up our krb5.conf with a base configuration. */
     tmpdir = test_tmpdir();
     setup_argv[0] = test_file_path("data/make-krb5-conf");
     if (setup_argv[0] == NULL)
         bail("cannot find data/make-krb5-conf in the test suite");
     setup_argv[1] = path;
     setup_argv[2] = tmpdir;
-    setup_argv[3] = (char *) "password_dictionary";
-    setup_argv[4] = dictionary;
-    setup_argv[5] = NULL;
-    run_setup((const char **) setup_argv);
+    setup_argv[3] = NULL;
 
     /* Point KRB5_CONFIG at the newly-generated krb5.conf file. */
     basprintf(&krb5_config, "KRB5_CONFIG=%s/krb5.conf", tmpdir);
     putenv(krb5_config);
     free(krb5_config_empty);
 
+#ifdef HAVE_CRACKLIB
+
+    /* Add CrackLib configuration. */
+    setup_argv[3] = (char *) "password_dictionary";
+    setup_argv[4] = dictionary;
+    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);
@@ -271,14 +284,22 @@ main(void)
         is_password_test(ctx, vtable, data, &length_tests[i]);
     vtable->close(ctx, data);
 
-    /* Add simple character class configuration to krb5.conf. */
-    setup_argv[5] = (char *) "minimum_different";
-    setup_argv[6] = (char *) "8";
-    setup_argv[7] = (char *) "require_ascii_printable";
+#else
+
+    /* Otherwise mark the CrackLib tests as skipped. */
+    count = ARRAY_SIZE(cracklib_tests) + ARRAY_SIZE(length_tests);
+    skip_block(count * 2 + 1, "not built with CrackLib support");
+
+#endif /* !HAVE_CRACKLIB */
+
+    /* Switch to simple character class configuration in krb5.conf. */
+    setup_argv[3] = (char *) "minimum_different";
+    setup_argv[4] = (char *) "8";
+    setup_argv[5] = (char *) "require_ascii_printable";
+    setup_argv[6] = (char *) "true";
+    setup_argv[7] = (char *) "require_non_letter";
     setup_argv[8] = (char *) "true";
-    setup_argv[9] = (char *) "require_non_letter";
-    setup_argv[10] = (char *) "true";
-    setup_argv[11] = NULL;
+    setup_argv[9] = NULL;
     run_setup((const char **) setup_argv);
 
     /* Obtain a new Kerberos context with that krb5.conf file. */
@@ -296,10 +317,7 @@ main(void)
         is_password_test(ctx, vtable, data, &letter_tests[i]);
     vtable->close(ctx, data);
 
-    /*
-     * Add complex character class configuration to krb5.conf but drop
-     * the dictionary configuration.
-     */
+    /* Add complex character class configuration to krb5.conf. */
     setup_argv[3] = (char *) "require_classes";
     setup_argv[4] = (char *) "8-19:lower,upper 8-15:digit 8-11:symbol 24-24:3";
     setup_argv[5] = NULL;
index e6924405c9e57e86cdfb2e73fe2ba786c894667b..5929bc3cfdf96c4b78b2c0994b1827c86d213202 100755 (executable)
@@ -3,7 +3,7 @@
 # Test suite for basic Heimdal external strength checking functionality.
 #
 # Written by Russ Allbery <eagle@eyrie.org>
-# Copyright 2016 Russ Allbery <eagle@eyrie.org>
+# Copyright 2016, 2017 Russ Allbery <eagle@eyrie.org>
 # Copyright 2009, 2012, 2013, 2014
 #     The Board of Trustees of the Leland Stanford Junior University
 #
@@ -24,6 +24,82 @@ use_prereq('JSON');
 use_prereq('Perl6::Slurp', 'slurp');
 use_prereq('Test::More',   '0.87_01');
 
+# Data directory to use for dictionaries.
+my $DATADIR = $ENV{BUILD} ? "$ENV{BUILD}/data" : 'tests/data';
+
+# This data structure drives most of our tests.  Each list element is a block
+# of tests to run together with a specific Kerberos configuration.  The keys
+# are:
+#
+# title  - Title of the tests for test output
+# config - Hash of Kerberos configuration to use
+# needs  - Dictionary type name we have to have to run this test
+# tests  - List of classes of tests to run (JSON files in tests/data/passwords)
+my @TESTS = (
+    {
+        title  => 'Generic tests',
+        config => {},
+        tests  => [qw(principal)],
+    },
+    {
+        title  => 'CrackLib tests',
+        config => {
+            password_dictionary => "$DATADIR/dictionary",
+        },
+        needs => 'CrackLib',
+        tests => [qw(cracklib principal)],
+    },
+    {
+        title  => 'Password length tests',
+        config => {
+            minimum_length => 12,
+        },
+        tests => [qw(length)],
+    },
+    {
+        title  => 'Password length tests with cracklib_maxlen',
+        config => {
+            password_dictionary => "$DATADIR/dictionary",
+            minimum_length      => 12,
+            cracklib_maxlen     => 11,
+        },
+        needs => 'CrackLib',
+        tests => [qw(length)],
+    },
+    {
+        title  => 'Simple password character class tests',
+        config => {
+            minimum_different       => 8,
+            require_ascii_printable => 'true',
+            require_non_letter      => 'true',
+        },
+        tests => [qw(letter)],
+    },
+    {
+        title  => 'Complex password character class tests',
+        config => {
+            require_classes =>
+              '8-19:lower,upper 8-15:digit 8-11:symbol 24-24:3',
+        },
+        tests => [qw(classes)],
+    },
+    {
+        title  => 'CDB tests',
+        config => {
+            password_dictionary_cdb => test_file_path('data/wordlist.cdb'),
+        },
+        tests => [qw(cdb principal)],
+    },
+    {
+        title  => 'SQLite tests',
+        config => {
+            password_dictionary_sqlite =>
+              test_file_path('data/wordlist.sqlite'),
+        },
+        tests => [qw(sqlite principal)],
+    },
+);
+
 # Run the newly-built heimdal-strength command and return the status, output,
 # and error output as a list.  If told to expect an immediate error, does not
 # pass input to the process.
@@ -152,6 +228,48 @@ sub load_password_tests {
     return $json->decode($testdata);
 }
 
+# Run a block of password tests, handling krb5.conf setup and skipping tests
+# if required dictionary support isn't available.
+#
+# $spec_ref  - Test specification (from @TESTS)
+# $tests_ref - Hash structure containing all loaded password tests
+#
+# Returns: undef
+sub run_password_tests {
+    my ($spec_ref, $tests_ref) = @_;
+    my $krb5_conf = create_krb5_conf($spec_ref->{config});
+    local $ENV{KRB5_CONFIG} = $krb5_conf;
+    note($spec_ref->{title});
+
+    # If we need support for a type of dictionary, check for that and skip the
+    # tests if that dictionary wasn't supported.
+  SKIP: {
+        if ($spec_ref->{needs}) {
+            my $type = $spec_ref->{needs};
+            my ($status, undef, $err) = run_heimdal_strength('test', 'pass');
+            my $err_regex = qr{ not [ ] built [ ] with [ ] \Q$type\E }xms;
+            if ($status == 1 && $err =~ $err_regex) {
+                my $total = 0;
+                for my $block (@{ $spec_ref->{tests} }) {
+                    $total += scalar(@{ $tests_ref->{$block} });
+                }
+                skip("not built with $type support", $total * 3);
+            }
+        }
+
+        # Run the tests.
+        for my $block (@{ $spec_ref->{tests} }) {
+            if (scalar(@{ $spec_ref->{tests} }) > 1) {
+                note('... ', $block);
+            }
+            for my $test (@{ $tests_ref->{$block} }) {
+                check_password($test);
+            }
+        }
+    }
+    return;
+}
+
 # Test a required_classes syntax error.  Takes the string for required_classes
 # and verifies that the appropriate error message is returned.
 #
@@ -183,154 +301,34 @@ sub test_require_classes_syntax {
     return;
 }
 
-# Load the password tests from JSON.  Accumulate a total count of tests for
-# the testing plan.
-my (%tests, $count);
+# Load the password tests from JSON.
+my %tests;
 for my $type (qw(cdb classes cracklib length letter principal sqlite)) {
     my $tests = load_password_tests("$type.json");
     $tests{$type} = $tests;
-    $count += scalar(@{$tests});
-}
-
-# We run the principal tests three times, for CrackLib, CDB, and SQLite.
-$count += 2 * scalar(@{ $tests{principal} });
-
-# We run the length checks twice.
-$count += scalar(@{ $tests{length} });
-
-# We can now calculate our plan based on three tests for each password test,
-# plus 27 additional tests for error handling.
-plan(tests => $count * 3 + 27);
-
-# Install the krb5.conf file with a configuration pointing to the test
-# CrackLib dictionary.
-my $datadir = $ENV{BUILD} ? "$ENV{BUILD}/data" : 'tests/data';
-my $krb5_conf
-  = create_krb5_conf({ password_dictionary => "$datadir/dictionary" });
-local $ENV{KRB5_CONFIG} = $krb5_conf;
-
-# Run the CrackLib password tests and based-on-principal tests from JSON.
-note('CrackLib tests');
-for my $test (@{ $tests{cracklib} }) {
-    check_password($test);
 }
-note('Generic tests with CrackLib');
-for my $test (@{ $tests{principal} }) {
-    check_password($test);
-}
-
-# Install the krb5.conf file with a length restriction.
-$krb5_conf = create_krb5_conf({ minimum_length => 12 });
-local $ENV{KRB5_CONFIG} = $krb5_conf;
 
-# Run the password length checks.
-note('Password length checks');
-for my $test (@{ $tests{length} }) {
-    check_password($test);
-}
-
-# Add a CrackLib dictionary and a maximum password length setting.
-$krb5_conf = create_krb5_conf(
-    {
-        password_dictionary => "$datadir/dictionary",
-        minimum_length      => 12,
-        cracklib_maxlen     => 11,
-    }
-);
-local $ENV{KRB5_CONFIG} = $krb5_conf;
-
-# Run the length checks again.  They should have the same result, even though
-# there's a CrackLib dictionary, since the dictionary hit is above the minimum
-# length.
-note('Password length checks with cracklib_maxlen');
-for my $test (@{ $tests{length} }) {
-    check_password($test);
-}
-
-# Install the krb5.conf file for simple character class restrictions.
-$krb5_conf = create_krb5_conf(
-    {
-        minimum_different       => 8,
-        require_ascii_printable => 'true',
-        require_non_letter      => 'true',
+# Determine our plan based on the test blocks we run (there are three test
+# results for each password test), plus 27 additional tests for error
+# handling.
+my $count = 0;
+for my $spec_ref (@TESTS) {
+    for my $block (@{ $spec_ref->{tests} }) {
+        $count += scalar(@{ $tests{$block} });
     }
-);
-local $ENV{KRB5_CONFIG} = $krb5_conf;
-
-# Run the simple character class tests.
-note('Simple password character class checks');
-for my $test (@{ $tests{letter} }) {
-    check_password($test);
-}
-
-# Install the krb5.conf file for complex character class restrictions.
-my $classes = '8-19:lower,upper 8-15:digit 8-11:symbol 24-24:3';
-$krb5_conf = create_krb5_conf({ require_classes => $classes });
-local $ENV{KRB5_CONFIG} = $krb5_conf;
-
-# Run the complex character class tests.
-note('Complex password character class checks');
-for my $test (@{ $tests{classes} }) {
-    check_password($test);
 }
+plan(tests => $count * 3 + 27);
 
-# Install the krb5.conf file with configuration pointing to the CDB
-# dictionary.
-my $cdb_database = test_file_path('data/wordlist.cdb');
-$krb5_conf = create_krb5_conf({ password_dictionary_cdb => $cdb_database });
-local $ENV{KRB5_CONFIG} = $krb5_conf;
-
-# Check whether we were built with CDB support.  If so, run those tests.
-my ($status, $output, $err) = run_heimdal_strength('test', 'password');
-SKIP: {
-    if ($status == 1 && $err =~ m{ not [ ] built [ ] with [ ] CDB }xms) {
-        my $total = scalar(@{ $tests{cdb} }) + scalar(@{ $tests{principal} });
-        skip('not built with CDB support', $total * 3);
-    }
-
-    # Run the CDB and principal password tests from JSON.
-    note('CDB tests');
-    for my $test (@{ $tests{cdb} }) {
-        check_password($test);
-    }
-    note('Generic tests with CDB');
-    for my $test (@{ $tests{principal} }) {
-        check_password($test);
-    }
-}
-
-# Install the krb5.conf file with configuration pointing to the SQLite
-# dictionary.
-my $sqlite_database = test_file_path('data/wordlist.sqlite');
-$krb5_conf
-  = create_krb5_conf({ password_dictionary_sqlite => $sqlite_database });
-local $ENV{KRB5_CONFIG} = $krb5_conf;
-
-# Check whether we were built with SQLite support.  If so, run those tests.
-($status, $output, $err) = run_heimdal_strength('test', 'password');
-SKIP: {
-    if ($status == 1 && $err =~ m{ not [ ] built [ ] with [ ] SQLite }xms) {
-        my $total = scalar(@{ $tests{sqlite} });
-        $total += scalar(@{ $tests{principal} });
-        skip('not built with SQLite support', $total * 3);
-    }
-
-    # Run the SQLite and principal password tests from JSON.
-    note('SQLite tests');
-    for my $test (@{ $tests{sqlite} }) {
-        check_password($test);
-    }
-    note('Generic tests with SQLite');
-    for my $test (@{ $tests{principal} }) {
-        check_password($test);
-    }
+# Run all the tests.
+for my $spec_ref (@TESTS) {
+    run_password_tests($spec_ref, \%tests);
 }
 
 # Test error for an unknown character class.
-$krb5_conf = create_krb5_conf({ require_classes => 'bogus' });
+my $krb5_conf = create_krb5_conf({ require_classes => 'bogus' });
 local $ENV{KRB5_CONFIG} = $krb5_conf;
 my $error_prefix = 'Cannot initialize strength checking';
-($status, $output, $err) = run_heimdal_strength('test', 'password', 1);
+my ($status, $output, $err) = run_heimdal_strength('test', 'password', 1);
 is($status, 1,   'Bad character class (status)');
 is($output, q{}, '...no output');
 is($err, "$error_prefix: unknown character class bogus\n", '...correct error');