From e086b834f43b417007e3f0b0a822db2a2350c2a9 Mon Sep 17 00:00:00 2001 From: William Brown Date: Apr 26 2017 00:40:51 +0000 Subject: Ticket 49135 - PBKDF2 should determine rounds at startup Bug Description: We used a hardcoded number of rounds for PBKDF2 Fix Description: Rather than hardcoding rounds at startup, we define an attacker "work" factor. We have chosen 40 milliseconds for now. Based on this factor, we then run a test to determine the CPU performance of the system. If the CPU performance is belowe a threshold, we use 10,000 rounds. If it is above, we scale the rounds up to our work factor. This way, each attempt by an attacker on a password should take 40 milliseconds - enough to cause them headaches, but still have a fast ldap server (given a bind takes about 500 milliseconds on my laptop today). https://pagure.io/389-ds-base/issue/49135 Author: wibrown Review by: mreynolds (Thanks!!) --- diff --git a/Makefile.am b/Makefile.am index bedccbf..e2421ab 100644 --- a/Makefile.am +++ b/Makefile.am @@ -2008,13 +2008,19 @@ test_slapd_SOURCES = test/main.c \ test/libslapd/pblock/analytics.c \ test/libslapd/pblock/v3_compat.c \ test/libslapd/operation/v3_compat.c \ - test/libslapd/spal/meminfo.c + test/libslapd/spal/meminfo.c \ + test/plugins/test.c \ + test/plugins/pwdstorage/pbkdf2.c -test_slapd_LDADD = libslapd.la +# We need to link a lot of plugins for this test. +test_slapd_LDADD = libslapd.la \ + libpwdstorage-plugin.la test_slapd_LDFLAGS = $(AM_CPPFLAGS) $(CMOCKA_LINKS) ### WARNING: Slap.h needs cert.h, which requires the -I/lib/ldaputil!!! ### WARNING: Slap.h pulls ssl.h, which requires nss!!!! -test_slapd_CPPFLAGS = $(AM_CPPFLAGS) $(DSPLUGIN_CPPFLAGS) $(DSINTERNAL_CPPFLAGS) +# We need to pull in plugin header paths too: +test_slapd_CPPFLAGS = $(AM_CPPFLAGS) $(DSPLUGIN_CPPFLAGS) $(DSINTERNAL_CPPFLAGS) \ + -I$(srcdir)/ldap/servers/plugins/pwdstorage test_libsds_SOURCES = src/libsds/test/test_sds.c \ src/libsds/test/test_sds_bpt.c \ diff --git a/dirsrvtests/tests/suites/password/pwd_algo_test.py b/dirsrvtests/tests/suites/password/pwd_algo_test.py index 4b3fb33..b3f03fe 100644 --- a/dirsrvtests/tests/suites/password/pwd_algo_test.py +++ b/dirsrvtests/tests/suites/password/pwd_algo_test.py @@ -69,9 +69,20 @@ def test_pwd_algo_test(topology_st): password conditions. """ - for algo in ( - 'CLEAR', 'CRYPT', 'MD5', 'SHA', 'SHA256', 'SHA384', 'SHA512', 'SMD5', 'SSHA', 'SSHA256', 'SSHA384', - 'SSHA512'): + for algo in ('CLEAR', + 'CRYPT', + 'MD5', + 'SHA', + 'SHA256', + 'SHA384', + 'SHA512', + 'SMD5', + 'SSHA', + 'SSHA256', + 'SSHA384', + 'SSHA512', + 'PBKDF2_SHA256', + ): _test_algo(topology_st.standalone, algo) log.info('Test PASSED') diff --git a/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c b/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c index b228700..f08b6ab 100644 --- a/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c +++ b/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c @@ -44,7 +44,10 @@ * At the same time we MUST increase this with each version of Directory Server * This value is written into the hash, so it's safe to change. */ -#define PBKDF2_ITERATIONS 30000 + +#define PBKDF2_MILLISECONDS 40 + +static PRUint32 PBKDF2_ITERATIONS = 30000; static const char *schemeName = PBKDF2_SHA256_SCHEME_NAME; static const PRUint32 schemeNameLength = PBKDF2_SHA256_NAME_LEN; @@ -52,6 +55,10 @@ static const PRUint32 schemeNameLength = PBKDF2_SHA256_NAME_LEN; /* For requesting the slot which supports these types */ static CK_MECHANISM_TYPE mechanism_array[] = {CKM_SHA256_HMAC, CKM_PKCS5_PBKD2}; +/* Used in our startup benching code */ +#define PBKDF2_BENCH_ROUNDS 50000 +#define PBKDF2_BENCH_LOOP 10 + void pbkdf2_sha256_extract(char *hash_in, SECItem *salt, PRUint32 *iterations) { @@ -124,12 +131,11 @@ pbkdf2_sha256_hash(char *hash_out, size_t hash_out_len, SECItem *pwd, SECItem *s } char * -pbkdf2_sha256_pw_enc(const char *pwd) +pbkdf2_sha256_pw_enc_rounds(const char *pwd, PRUint32 iterations) { char hash[ PBKDF2_TOTAL_LENGTH ]; size_t encsize = 3 + schemeNameLength + LDIF_BASE64_LEN(PBKDF2_TOTAL_LENGTH); char *enc = slapi_ch_calloc(encsize, sizeof(char)); - PRUint32 iterations = PBKDF2_ITERATIONS; SECItem saltItem; SECItem passItem; @@ -174,24 +180,24 @@ pbkdf2_sha256_pw_enc(const char *pwd) return enc; } +char * +pbkdf2_sha256_pw_enc(const char *pwd) { + return pbkdf2_sha256_pw_enc_rounds(pwd, PBKDF2_ITERATIONS); +} + PRInt32 pbkdf2_sha256_pw_cmp(const char *userpwd, const char *dbpwd) { PRInt32 result = 1; /* Default to fail. */ - char dbhash[ PBKDF2_TOTAL_LENGTH ]; - char userhash[ PBKDF2_HASH_LENGTH ]; + char dbhash[ PBKDF2_TOTAL_LENGTH ] = {0}; + char userhash[ PBKDF2_HASH_LENGTH ] = {0}; PRUint32 dbpwd_len = strlen(dbpwd); SECItem saltItem; SECItem passItem; PRUint32 iterations = 0; - /* Our hash value is always at a known offset. */ - char *hash = dbhash + PBKDF2_ITERATIONS_LENGTH + PBKDF2_SALT_LENGTH; - slapi_log_err(SLAPI_LOG_PLUGIN, (char *)schemeName, "Comparing password\n"); - memset(dbhash, 0, PBKDF2_TOTAL_LENGTH); - passItem.data = (unsigned char *)userpwd; passItem.len = strlen(userpwd); @@ -208,10 +214,101 @@ pbkdf2_sha256_pw_cmp(const char *userpwd, const char *dbpwd) slapi_log_err(SLAPI_LOG_ERR, (char *)schemeName, "Unable to hash userpwd value\n"); return result; } + + /* Our hash value is always at a known offset in the decoded string. */ + char *hash = dbhash + PBKDF2_ITERATIONS_LENGTH + PBKDF2_SALT_LENGTH; + /* Now compare the result of pbkdf2_sha256_hash. */ result = memcmp(userhash, hash, PBKDF2_HASH_LENGTH); return result; } +uint64_t +pbkdf2_sha256_benchmark_iterations() { + /* Time how long it takes to do PBKDF2_BENCH_LOOP attempts of PBKDF2_BENCH_ROUNDS rounds */ + uint64_t time_nsec = 0; + char *results[PBKDF2_BENCH_LOOP] = {0}; + struct timespec start_time; + struct timespec finish_time; + + clock_gettime(CLOCK_MONOTONIC, &start_time); + + for (size_t i = 0; i < PBKDF2_BENCH_LOOP; i++) { + results[i] = pbkdf2_sha256_pw_enc_rounds("Eequee9mutheuchiehe4", PBKDF2_BENCH_ROUNDS); + } + + clock_gettime(CLOCK_MONOTONIC, &finish_time); + + for (size_t i = 0; i < PBKDF2_BENCH_LOOP; i++) { + slapi_ch_free((void **)&(results[i])); + } + + /* Work out the execution time. */ + time_nsec = (finish_time.tv_sec - start_time.tv_sec) * 1000000000; + if (finish_time.tv_nsec > start_time.tv_nsec) { + time_nsec += finish_time.tv_nsec - start_time.tv_nsec; + } else { + time_nsec += 1000000000 - (start_time.tv_nsec - finish_time.tv_nsec); + } + + time_nsec = time_nsec / PBKDF2_BENCH_LOOP; + + return time_nsec; +} + +PRUint32 +pbkdf2_sha256_calculate_iterations(uint64_t time_nsec) { + /* + * So we know that we have nsec for a single round of PBKDF2_BENCH_ROUNDS now. + * first, we get the cost of "every 1000 rounds" + */ + uint64_t number_thou_rounds = PBKDF2_BENCH_ROUNDS / 1000; + uint64_t thou_time_nsec = time_nsec / number_thou_rounds; + + /* + * Now we have the cost of 1000 rounds. Now, knowing this we say + * we want an attacker to have to expend say ... example 8 ms of work + * to try a password. So this is 1,000,000 ns = 1ms, ergo + * 8,000,000 + */ + uint64_t attack_work_nsec = PBKDF2_MILLISECONDS * 1000000; + + /* + * Knowing the attacker time and our cost, we can divide this + * to get how many thousands of rounds we should use. + */ + uint64_t thou_rounds = (attack_work_nsec / thou_time_nsec); + + /* + * Finally, we make the rounds in terms of thousands, and cast it. + */ + PRUint32 final_rounds = thou_rounds * 1000; + + if (final_rounds < 10000) { + final_rounds = 10000; + } + + return final_rounds; +} + + +int +pbkdf2_sha256_start(Slapi_PBlock *pb __attribute__((unused))) { + /* Run the time generator */ + uint64_t time_nsec = pbkdf2_sha256_benchmark_iterations(); + /* Calculate the iterations */ + /* set it globally */ + PBKDF2_ITERATIONS = pbkdf2_sha256_calculate_iterations(time_nsec); + /* Make a note of it. */ + slapi_log_err(SLAPI_LOG_PLUGIN, (char *)schemeName, "Based on CPU performance, chose %"PRIu32" rounds\n", PBKDF2_ITERATIONS); + return 0; +} + +/* Do we need the matching close function? */ +int +pbkdf2_sha256_close(Slapi_PBlock *pb __attribute__((unused))) { + return 0; +} + diff --git a/ldap/servers/plugins/pwdstorage/pwd_init.c b/ldap/servers/plugins/pwdstorage/pwd_init.c index 0781c09..16d2f32 100644 --- a/ldap/servers/plugins/pwdstorage/pwd_init.c +++ b/ldap/servers/plugins/pwdstorage/pwd_init.c @@ -349,6 +349,8 @@ pbkdf2_sha256_pwd_storage_scheme_init(Slapi_PBlock *pb) rc = slapi_pblock_set(pb, SLAPI_PLUGIN_VERSION, (void *) SLAPI_PLUGIN_VERSION_01); rc |= slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION, (void *)&pbkdf2_sha256_pdesc); + rc |= slapi_pblock_set(pb, SLAPI_PLUGIN_START_FN, (void*)&pbkdf2_sha256_start); + rc |= slapi_pblock_set(pb, SLAPI_PLUGIN_CLOSE_FN, (void*)&pbkdf2_sha256_close); rc |= slapi_pblock_set(pb, SLAPI_PLUGIN_PWD_STORAGE_SCHEME_ENC_FN, (void *)pbkdf2_sha256_pw_enc); rc |= slapi_pblock_set(pb, SLAPI_PLUGIN_PWD_STORAGE_SCHEME_CMP_FN, (void *)pbkdf2_sha256_pw_cmp); rc |= slapi_pblock_set(pb, SLAPI_PLUGIN_PWD_STORAGE_SCHEME_NAME, PBKDF2_SHA256_SCHEME_NAME); diff --git a/ldap/servers/plugins/pwdstorage/pwdstorage.h b/ldap/servers/plugins/pwdstorage/pwdstorage.h index d48b634..2909998 100644 --- a/ldap/servers/plugins/pwdstorage/pwdstorage.h +++ b/ldap/servers/plugins/pwdstorage/pwdstorage.h @@ -7,22 +7,20 @@ * See LICENSE for details. * END COPYRIGHT BLOCK **/ +#pragma once + #ifdef HAVE_CONFIG_H # include #endif -#ifndef _PWDSTORAGE_H -#define _PWDSTORAGE_H - -#include "slapi-plugin.h" -#include "slapi-private.h" +#include +#include #include -#include "nspr.h" -#include "plbase64.h" -#include "ldif.h" +#include +#include +#include #include "md5.h" - #define PWD_HASH_PREFIX_START '{' #define PWD_HASH_PREFIX_END '}' @@ -86,11 +84,16 @@ char *md5_pw_enc( const char *pwd ); int smd5_pw_cmp( const char *userpwd, const char *dbpwd ); char *smd5_pw_enc( const char *pwd ); +int pbkdf2_sha256_start(Slapi_PBlock *pb); +int pbkdf2_sha256_close(Slapi_PBlock *pb); SECStatus pbkdf2_sha256_hash(char *hash_out, size_t hash_out_len, SECItem *pwd, SECItem *salt, PRUint32 iterations); char * pbkdf2_sha256_pw_enc(const char *pwd); int pbkdf2_sha256_pw_cmp(const char *userpwd, const char *dbpwd); +/* For testing pbkdf2 only */ +uint64_t pbkdf2_sha256_benchmark_iterations(); +PRUint32 pbkdf2_sha256_calculate_iterations(); + /* Utility functions */ PRUint32 pwdstorage_base64_decode_len(const char *encval, PRUint32 enclen); -#endif /* _PWDSTORAGE_H */ diff --git a/test/main.c b/test/main.c index d84f8df..36447ea 100644 --- a/test/main.c +++ b/test/main.c @@ -12,5 +12,8 @@ int main ( int argc __attribute__((unused)), char **argv __attribute__((unused))) { int result = 0; result += run_libslapd_tests(); + result += run_plugin_tests(); + + PR_Cleanup(); return result; } diff --git a/test/plugins/pwdstorage/pbkdf2.c b/test/plugins/pwdstorage/pbkdf2.c new file mode 100644 index 0000000..abadbfb --- /dev/null +++ b/test/plugins/pwdstorage/pbkdf2.c @@ -0,0 +1,73 @@ +/** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2017 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). + * See LICENSE for details. + * END COPYRIGHT BLOCK **/ + +#include "../../test_slapd.h" + +#include +#include + +int +test_plugin_pwdstorage_nss_setup(void **state __attribute__((unused))) { + int result = NSS_Initialize(NULL, "", "", SECMOD_DB, NSS_INIT_READONLY|NSS_INIT_NOCERTDB|NSS_INIT_NOMODDB); + assert_true(result == 0); + return result; +} + +int +test_plugin_pwdstorage_nss_stop(void **state __attribute__((unused))) { + NSS_Shutdown(); + return 0; +} + +void +test_plugin_pwdstorage_pbkdf2_auth(void **state __attribute__((unused))) { + + /* Check that given various known passwords and hashes they validate (or don't) */ + + /* 'password' */ + const char *password_a = "AAB1MHPzX9ZP+HDQYp/+qxQwJAW5cXhRvXX1+w0NBMVX6FyMv2uzIvtBfvn6A3o84gKW9fBl5hGPeH87bQMZs977SvCV09P8MV/fkkjH7EoYNXoSQ6FFBpjm3orFplT9Y5PY14xRvJS4iicQ82uKaaARlkbn0uLaHBNS18uz1YFzuYUlf4lqh+uy1VzAR3YQW9FWKL9TYCsTRx75EGUMYj/f7826CqrHNubnljh4s5gi31y+2qsdzdRerT1ISZC5z0kQbkXZYM7UCa4hlbSQl3mO6lpyxk44oiPkbKKii+bS+KRdIMeMgFawXo2L4+IYx+qXvJRwyi1M8vIxK+dnc2kOrLF9E7rZvs0hn9PuXMW3Itq46wPL3R51wo+0ki4gA36ZNF3PegbjFiAvrh24/D3SQMBjfk1YMDstNGJaMefd3bS1"; + + /* + * 'password' - but we mucked with the rounds + * note the 5th char of the b64 is "L" not "M'. This changes the rounds from + * 30000 to 29996, which means we should fail + */ + const char *password_a_rounds = "AAB1LHPzX9ZP+HDQYp/+qxQwJAW5cXhRvXX1+w0NBMVX6FyMv2uzIvtBfvn6A3o84gKW9fBl5hGPeH87bQMZs977SvCV09P8MV/fkkjH7EoYNXoSQ6FFBpjm3orFplT9Y5PY14xRvJS4iicQ82uKaaARlkbn0uLaHBNS18uz1YFzuYUlf4lqh+uy1VzAR3YQW9FWKL9TYCsTRx75EGUMYj/f7826CqrHNubnljh4s5gi31y+2qsdzdRerT1ISZC5z0kQbkXZYM7UCa4hlbSQl3mO6lpyxk44oiPkbKKii+bS+KRdIMeMgFawXo2L4+IYx+qXvJRwyi1M8vIxK+dnc2kOrLF9E7rZvs0hn9PuXMW3Itq46wPL3R51wo+0ki4gA36ZNF3PegbjFiAvrh24/D3SQMBjfk1YMDstNGJaMefd3bS1"; + + /* + * 'password' - but we mucked with the salt Note the change in the 8th char from + * z to 0. + */ + const char *password_a_salt = "AAB1MHP0X9ZP+HDQYp/+qxQwJAW5cXhRvXX1+w0NBMVX6FyMv2uzIvtBfvn6A3o84gKW9fBl5hGPeH87bQMZs977SvCV09P8MV/fkkjH7EoYNXoSQ6FFBpjm3orFplT9Y5PY14xRvJS4iicQ82uKaaARlkbn0uLaHBNS18uz1YFzuYUlf4lqh+uy1VzAR3YQW9FWKL9TYCsTRx75EGUMYj/f7826CqrHNubnljh4s5gi31y+2qsdzdRerT1ISZC5z0kQbkXZYM7UCa4hlbSQl3mO6lpyxk44oiPkbKKii+bS+KRdIMeMgFawXo2L4+IYx+qXvJRwyi1M8vIxK+dnc2kOrLF9E7rZvs0hn9PuXMW3Itq46wPL3R51wo+0ki4gA36ZNF3PegbjFiAvrh24/D3SQMBjfk1YMDstNGJaMefd3bS1"; + + assert_true(pbkdf2_sha256_pw_cmp("password", password_a) == 0); + assert_false(pbkdf2_sha256_pw_cmp("password", password_a_rounds) == 0); + assert_false(pbkdf2_sha256_pw_cmp("password", password_a_salt) == 0); + assert_false(pbkdf2_sha256_pw_cmp("password_b", password_a) == 0); +} + +void +test_plugin_pwdstorage_pbkdf2_rounds(void **state __attribute__((unused))){ + /* Check the benchmark, and make sure we get a valid timestamp */ + assert_true(pbkdf2_sha256_benchmark_iterations() > 0); + /* + * provide various values to the calculator, to check we get the right + * number of rounds back. + */ + /* + * On a very slow system, we get the default min rounds out. + */ + assert_true(pbkdf2_sha256_calculate_iterations(1000000000) == 10000); + /* + * On a "fast" system, we should see more rounds. + */ + assert_true(pbkdf2_sha256_calculate_iterations(200000000) == 10000); + assert_true(pbkdf2_sha256_calculate_iterations(100000000) == 20000); + assert_true(pbkdf2_sha256_calculate_iterations(50000000) == 40000); +} + diff --git a/test/plugins/test.c b/test/plugins/test.c new file mode 100644 index 0000000..d853913 --- /dev/null +++ b/test/plugins/test.c @@ -0,0 +1,31 @@ +/** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2017 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). + * See LICENSE for details. + * END COPYRIGHT BLOCK **/ + +#include "../test_slapd.h" + +void +test_plugin_hello(void **state __attribute__((unused))) { + /* It works! */ + assert_int_equal(1, 1); +} + +int +run_plugin_tests (void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_plugin_hello), + cmocka_unit_test_setup_teardown(test_plugin_pwdstorage_pbkdf2_auth, + test_plugin_pwdstorage_nss_setup, + test_plugin_pwdstorage_nss_stop), + cmocka_unit_test_setup_teardown(test_plugin_pwdstorage_pbkdf2_rounds, + test_plugin_pwdstorage_nss_setup, + test_plugin_pwdstorage_nss_stop), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} + + diff --git a/test/test_slapd.h b/test/test_slapd.h index 50de11b..2c2b9f5 100644 --- a/test/test_slapd.h +++ b/test/test_slapd.h @@ -19,6 +19,7 @@ /* Test runners */ int run_libslapd_tests (void); +int run_plugin_tests (void); /* == The tests == */ @@ -47,3 +48,15 @@ void test_libslapd_counters_atomic_overflow(void **state); void test_libslapd_pal_meminfo(void **state); void test_libslapd_util_cachesane(void **state); +/* plugins */ + +void test_plugin_hello(void **state); + +/* plugin-pwdstorage-pbkdf2 */ + +int test_plugin_pwdstorage_nss_setup(void **state); +int test_plugin_pwdstorage_nss_stop(void **state); + +void test_plugin_pwdstorage_pbkdf2_auth(void **state); +void test_plugin_pwdstorage_pbkdf2_rounds(void **state); +