2 * Test for the MIT Kerberos shared module API.
4 * Written by Russ Allbery <eagle@eyrie.org>
5 * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org>
6 * Copyright 2010, 2013-2014
7 * The Board of Trustees of the Leland Stanford Junior University
9 * SPDX-License-Identifier: MIT
13 #include <portable/kadmin.h>
14 #include <portable/krb5.h>
15 #include <portable/system.h>
19 #ifdef HAVE_KRB5_PWQUAL_PLUGIN_H
20 # include <krb5/pwqual_plugin.h>
23 #include <tests/tap/basic.h>
24 #include <tests/tap/kerberos.h>
25 #include <tests/tap/process.h>
26 #include <tests/tap/string.h>
27 #include <util/macros.h>
30 * The password test data, generated from the JSON source. Defines arrays
31 * named *_tests, where * is the file name without the ".c" suffix.
33 #include <tests/data/passwords/cdb.c>
34 #include <tests/data/passwords/classes.c>
35 #include <tests/data/passwords/cracklib.c>
36 #include <tests/data/passwords/length.c>
37 #include <tests/data/passwords/letter.c>
38 #include <tests/data/passwords/principal.c>
39 #include <tests/data/passwords/sqlite.c>
42 #ifndef HAVE_KRB5_PWQUAL_PLUGIN_H
44 * If we're not building with MIT Kerberos, we can't run this test and much of
45 * the test won't even compile. Replace this test with a small program that
46 * just calls skip_all.
51 skip_all("not built against MIT libraries");
57 /* The public symbol that we load and call to get the vtable. */
58 typedef krb5_error_code pwqual_strength_initvt(krb5_context, int, int,
63 * Loads the Heimdal password change plugin and tests that its metadata is
64 * correct. Returns a pointer to the kadm5_pw_policy_verifier struct or bails
65 * on failure to load the plugin. Stores the handle from dlopen in its second
66 * argument for a later clean shutdown.
68 static krb5_pwqual_vtable
69 load_plugin(krb5_context ctx, void **handle)
73 krb5_pwqual_vtable vtable = NULL;
74 pwqual_strength_initvt *init;
76 /* Load the module. */
77 path = test_file_path("../plugin/.libs/strength.so");
79 bail("cannot find plugin");
80 *handle = dlopen(path, RTLD_NOW);
82 bail("cannot dlopen %s: %s", path, dlerror());
83 test_file_path_free(path);
85 /* Find the entry point function. */
86 init = (pwqual_strength_initvt *) dlsym(*handle, "pwqual_strength_initvt");
88 bail("cannot get pwqual_strength_initvt symbol: %s", dlerror());
90 /* Test for correct results when requesting the wrong API version. */
91 code = init(ctx, 2, 0, (krb5_plugin_vtable) vtable);
92 is_int(code, KRB5_PLUGIN_VER_NOTSUPP,
93 "Correct status for bad major API version");
95 /* Call that function properly to get the vtable. */
96 vtable = bmalloc(sizeof(*vtable));
97 code = init(ctx, 1, 1, (krb5_plugin_vtable) vtable);
99 bail_krb5(ctx, code, "cannot obtain module vtable");
101 /* Check that all of the vtable entries are present. */
102 if (vtable->open == NULL || vtable->check == NULL || vtable->close == NULL)
103 bail("missing function in module vtable");
105 /* Verify the metadata. */
106 is_string("krb5-strength", vtable->name, "Module name");
108 /* Return the vtable. */
114 * Given a Kerberos context, the dispatch table, the module data, and a test
115 * case, call out to the password strength checking module and check the
119 is_password_test(krb5_context ctx, const krb5_pwqual_vtable vtable,
120 krb5_pwqual_moddata data, const struct password_test *test)
122 krb5_principal princ;
123 krb5_error_code code;
126 /* Translate the principal into a krb5_principal. */
127 code = krb5_parse_name(ctx, test->principal, &princ);
129 bail_krb5(ctx, code, "cannot parse principal %s", test->principal);
131 /* Call the verifier. */
132 code = vtable->check(ctx, data, test->password, NULL, princ, NULL);
134 /* Check the results against the test data. */
135 is_int(test->code, code, "%s (status)", test->name);
137 is_string(test->error, NULL, "%s (error)", test->name);
139 error = krb5_get_error_message(ctx, code);
140 is_string(test->error, error, "%s (error)", test->name);
141 krb5_free_error_message(ctx, error);
144 /* Free the parsed principal. */
145 krb5_free_principal(ctx, princ);
152 char *path, *dictionary, *krb5_config, *krb5_config_empty, *tmpdir;
153 char *setup_argv[12];
157 krb5_pwqual_vtable vtable;
158 krb5_pwqual_moddata data;
159 krb5_error_code code;
163 * Calculate how many tests we have. There are two tests for the module
164 * metadata, seven more tests for initializing the plugin, and two tests
167 * We run all the CrackLib tests twice, once with an explicit dictionary
168 * path and once from krb5.conf configuration. We run the principal tests
169 * with CrackLib, CDB, and SQLite configurations.
171 count = 2 * ARRAY_SIZE(cracklib_tests);
172 count += 2 * ARRAY_SIZE(length_tests);
173 count += ARRAY_SIZE(cdb_tests);
174 count += ARRAY_SIZE(sqlite_tests);
175 count += ARRAY_SIZE(classes_tests);
176 count += ARRAY_SIZE(letter_tests);
177 count += 3 * ARRAY_SIZE(principal_tests);
178 plan(2 + 8 + count * 2);
180 /* Start with the krb5.conf that contains no dictionary configuration. */
181 path = test_file_path("data/krb5.conf");
183 bail("cannot find data/krb5.conf in the test suite");
184 basprintf(&krb5_config_empty, "KRB5_CONFIG=%s", path);
185 putenv(krb5_config_empty);
187 /* Obtain a Kerberos context with that krb5.conf file. */
188 code = krb5_init_context(&ctx);
190 bail_krb5(ctx, code, "cannot initialize Kerberos context");
192 /* Load the plugin. */
193 vtable = load_plugin(ctx, &handle);
195 /* Initialize the plugin with a CrackLib dictionary. */
196 build = getenv("BUILD");
198 bail("BUILD not set in the environment");
199 basprintf(&dictionary, "%s/data/dictionary", build);
200 code = vtable->open(ctx, dictionary, &data);
201 is_int(0, code, "Plugin initialization (explicit dictionary)");
203 bail("cannot continue after plugin initialization failure");
205 /* Run the principal tests. */
206 for (i = 0; i < ARRAY_SIZE(principal_tests); i++)
207 is_password_test(ctx, vtable, data, &principal_tests[i]);
209 # ifdef HAVE_CRACKLIB
211 * Run the CrackLib tests if CrackLib is available, otherwise skip them.
212 * If built with the system CrackLib, skip tests that are marked as only
213 * working with the tougher rules of our embedded CrackLib.
215 for (i = 0; i < ARRAY_SIZE(cracklib_tests); i++) {
216 # ifdef HAVE_SYSTEM_CRACKLIB
217 if (cracklib_tests[i].skip_for_system_cracklib) {
218 skip_block(2, "not built with embedded CrackLib");
222 is_password_test(ctx, vtable, data, &cracklib_tests[i]);
225 count = ARRAY_SIZE(cracklib_tests);
226 skip_block(count * 2, "not built with CrackLib support");
229 /* Close that initialization of the plugin and destroy that context. */
230 vtable->close(ctx, data);
231 krb5_free_context(ctx);
234 /* Set up our krb5.conf with a base configuration. */
235 tmpdir = test_tmpdir();
236 setup_argv[0] = test_file_path("data/make-krb5-conf");
237 if (setup_argv[0] == NULL)
238 bail("cannot find data/make-krb5-conf in the test suite");
239 setup_argv[1] = path;
240 setup_argv[2] = tmpdir;
242 /* Point KRB5_CONFIG at the newly-generated krb5.conf file. */
243 basprintf(&krb5_config, "KRB5_CONFIG=%s/krb5.conf", tmpdir);
245 free(krb5_config_empty);
247 # ifdef HAVE_CRACKLIB
249 /* Add CrackLib configuration. */
250 setup_argv[3] = (char *) "password_dictionary";
251 setup_argv[4] = dictionary;
252 setup_argv[5] = NULL;
253 run_setup((const char **) setup_argv);
255 /* Obtain a new Kerberos context with that krb5.conf file. */
256 krb5_free_context(ctx);
257 code = krb5_init_context(&ctx);
259 bail_krb5(ctx, code, "cannot initialize Kerberos context");
261 /* Run all of the tests again. No need to re-run principal tests. */
262 code = vtable->open(ctx, NULL, &data);
263 is_int(0, code, "Plugin initialization (krb5.conf dictionary)");
265 bail("cannot continue after plugin initialization failure");
266 for (i = 0; i < ARRAY_SIZE(cracklib_tests); i++) {
267 # ifdef HAVE_SYSTEM_CRACKLIB
268 if (cracklib_tests[i].skip_for_system_cracklib) {
269 skip_block(2, "not built with embedded CrackLib");
273 is_password_test(ctx, vtable, data, &cracklib_tests[i]);
275 vtable->close(ctx, data);
278 * Add length restrictions and a maximum length for CrackLib. This should
279 * reject passwords as too short, but let through a password that's
280 * actually in the CrackLib dictionary.
282 setup_argv[5] = (char *) "minimum_length";
283 setup_argv[6] = (char *) "12";
284 setup_argv[7] = (char *) "cracklib_maxlen";
285 setup_argv[8] = (char *) "11";
286 setup_argv[9] = NULL;
287 run_setup((const char **) setup_argv);
289 /* Obtain a new Kerberos context with that krb5.conf file. */
290 krb5_free_context(ctx);
291 code = krb5_init_context(&ctx);
293 bail_krb5(ctx, code, "cannot initialize Kerberos context");
295 /* Run all of the length tests. */
296 code = vtable->open(ctx, NULL, &data);
297 is_int(0, code, "Plugin initialization (length)");
299 bail("cannot continue after plugin initialization failure");
300 for (i = 0; i < ARRAY_SIZE(length_tests); i++)
301 is_password_test(ctx, vtable, data, &length_tests[i]);
302 vtable->close(ctx, data);
306 /* Otherwise mark the CrackLib tests as skipped. */
307 count = ARRAY_SIZE(cracklib_tests) + ARRAY_SIZE(length_tests);
308 skip_block(count * 2 + 2, "not built with CrackLib support");
310 # endif /* !HAVE_CRACKLIB */
312 /* Switch to simple character class configuration in krb5.conf. */
313 setup_argv[3] = (char *) "minimum_different";
314 setup_argv[4] = (char *) "8";
315 setup_argv[5] = (char *) "require_ascii_printable";
316 setup_argv[6] = (char *) "true";
317 setup_argv[7] = (char *) "require_non_letter";
318 setup_argv[8] = (char *) "true";
319 setup_argv[9] = NULL;
320 run_setup((const char **) setup_argv);
322 /* Obtain a new Kerberos context with that krb5.conf file. */
323 krb5_free_context(ctx);
324 code = krb5_init_context(&ctx);
326 bail_krb5(ctx, code, "cannot initialize Kerberos context");
328 /* Run all the simple character class tests. */
329 code = vtable->open(ctx, NULL, &data);
330 is_int(0, code, "Plugin initialization (simple character class)");
332 bail("cannot continue after plugin initialization failure");
333 for (i = 0; i < ARRAY_SIZE(letter_tests); i++)
334 is_password_test(ctx, vtable, data, &letter_tests[i]);
335 vtable->close(ctx, data);
337 /* Add complex character class configuration to krb5.conf. */
338 setup_argv[3] = (char *) "require_classes";
339 setup_argv[4] = (char *) "8-19:lower,upper 8-15:digit 8-11:symbol 24-24:3";
340 setup_argv[5] = NULL;
341 run_setup((const char **) setup_argv);
343 /* Obtain a new Kerberos context with that krb5.conf file. */
344 krb5_free_context(ctx);
345 code = krb5_init_context(&ctx);
347 bail_krb5(ctx, code, "cannot initialize Kerberos context");
349 /* Run all the complex character class tests. */
350 code = vtable->open(ctx, NULL, &data);
351 is_int(0, code, "Plugin initialization (complex character class)");
353 bail_krb5(ctx, code, "plugin initialization failure");
354 for (i = 0; i < ARRAY_SIZE(classes_tests); i++)
355 is_password_test(ctx, vtable, data, &classes_tests[i]);
356 vtable->close(ctx, data);
358 /* Re-run the length restriction checks with no dictionary at all. */
359 setup_argv[3] = (char *) "minimum_length";
360 setup_argv[4] = (char *) "12";
361 setup_argv[5] = NULL;
362 run_setup((const char **) setup_argv);
364 /* Obtain a new Kerberos context with that krb5.conf file. */
365 krb5_free_context(ctx);
366 code = krb5_init_context(&ctx);
368 bail_krb5(ctx, code, "cannot initialize Kerberos context");
370 /* Run all of the length tests. */
371 code = vtable->open(ctx, NULL, &data);
372 is_int(0, code, "Plugin initialization (length)");
374 bail("cannot continue after plugin initialization failure");
375 for (i = 0; i < ARRAY_SIZE(length_tests); i++)
376 is_password_test(ctx, vtable, data, &length_tests[i]);
377 vtable->close(ctx, data);
381 /* If built with CDB, set up krb5.conf to use a CDB dictionary instead. */
382 test_file_path_free(dictionary);
383 dictionary = test_file_path("data/wordlist.cdb");
384 if (dictionary == NULL)
385 bail("cannot find data/wordlist.cdb in the test suite");
386 setup_argv[3] = (char *) "password_dictionary_cdb";
387 setup_argv[4] = dictionary;
388 setup_argv[5] = NULL;
389 run_setup((const char **) setup_argv);
391 /* Obtain a new Kerberos context with that krb5.conf file. */
392 krb5_free_context(ctx);
393 code = krb5_init_context(&ctx);
395 bail_krb5(ctx, code, "cannot initialize Kerberos context");
397 /* Run the CDB and principal tests. */
398 code = vtable->open(ctx, NULL, &data);
399 is_int(0, code, "Plugin initialization (CDB dictionary)");
401 bail("cannot continue after plugin initialization failure");
402 for (i = 0; i < ARRAY_SIZE(cdb_tests); i++)
403 is_password_test(ctx, vtable, data, &cdb_tests[i]);
404 for (i = 0; i < ARRAY_SIZE(principal_tests); i++)
405 is_password_test(ctx, vtable, data, &principal_tests[i]);
406 vtable->close(ctx, data);
408 # else /* !HAVE_CDB */
410 /* Otherwise, mark the CDB tests as skipped. */
411 count = ARRAY_SIZE(cdb_tests) + ARRAY_SIZE(principal_tests);
412 skip_block(count * 2 + 1, "not built with CDB support");
414 # endif /* !HAVE_CDB */
419 * If built with SQLite, set up krb5.conf to use a SQLite dictionary
422 test_file_path_free(dictionary);
423 dictionary = test_file_path("data/wordlist.sqlite");
424 if (dictionary == NULL)
425 bail("cannot find data/wordlist.sqlite in the test suite");
426 setup_argv[3] = (char *) "password_dictionary_sqlite";
427 setup_argv[4] = dictionary;
428 setup_argv[5] = NULL;
429 run_setup((const char **) setup_argv);
430 test_file_path_free(setup_argv[0]);
431 test_file_path_free(path);
433 /* Obtain a new Kerberos context with that krb5.conf file. */
434 krb5_free_context(ctx);
435 code = krb5_init_context(&ctx);
437 bail_krb5(ctx, code, "cannot initialize Kerberos context");
439 /* Run the SQLite and principal tests. */
440 code = vtable->open(ctx, NULL, &data);
441 is_int(0, code, "Plugin initialization (SQLite dictionary)");
443 bail("cannot continue after plugin initialization failure");
444 for (i = 0; i < ARRAY_SIZE(sqlite_tests); i++)
445 is_password_test(ctx, vtable, data, &sqlite_tests[i]);
446 for (i = 0; i < ARRAY_SIZE(principal_tests); i++)
447 is_password_test(ctx, vtable, data, &principal_tests[i]);
448 vtable->close(ctx, data);
450 # else /* !HAVE_SQLITE */
452 /* Otherwise, mark the SQLite tests as skipped. */
453 count = ARRAY_SIZE(sqlite_tests) + ARRAY_SIZE(principal_tests);
454 skip_block(count * 2 + 1, "not built with SQLite support");
456 # endif /* !HAVE_SQLITE */
458 /* Manually clean up after the results of make-krb5-conf. */
459 basprintf(&path, "%s/krb5.conf", tmpdir);
462 test_tmpdir_free(tmpdir);
464 /* Close down the module. */
465 if (dlclose(handle) != 0)
466 bail("cannot close plugin: %s", dlerror());
468 /* Keep valgrind clean by freeing all memory. */
469 test_file_path_free(dictionary);
470 krb5_free_context(ctx);
472 putenv((char *) "KRB5_CONFIG=");
477 #endif /* HAVE_KRB5_PWQUAL_PLUGIN_H */