From 9fde3a7508e099e0ae445c51916b7ead127bfd85 Mon Sep 17 00:00:00 2001 From: Jorge Pereira Date: Wed, 6 Sep 2023 17:40:09 -0300 Subject: [PATCH 01/12] rlm_totp: Backport changes from v3.2.x --- raddb/mods-available/totp | 23 ++++ src/modules/rlm_totp/rlm_totp.c | 189 ++++++++++++++++++++++++-------- 2 files changed, 169 insertions(+), 43 deletions(-) diff --git a/raddb/mods-available/totp b/raddb/mods-available/totp index b354122b64ec..43d2f57afa42 100644 --- a/raddb/mods-available/totp +++ b/raddb/mods-available/totp @@ -43,4 +43,27 @@ # The module takes no configuration items. # totp { + # + # Default time step between time changes + # + time_step = 30 + + # + # Length of the one-time password. + # + # Must be 6 or 8 + # + otp_length = 8 + + # + # How many steps backward in time we look for a matching OTP + # + lookback_steps = 1 + + # + # Time delta between steps. + # + # Cannot be larger than time_step + # + lookback_interval = 30 } diff --git a/src/modules/rlm_totp/rlm_totp.c b/src/modules/rlm_totp/rlm_totp.c index 8c283272defb..9e876bbb5f05 100644 --- a/src/modules/rlm_totp/rlm_totp.c +++ b/src/modules/rlm_totp/rlm_totp.c @@ -53,7 +53,35 @@ static const call_method_env_t method_env = { .env = call_env }; -#define TIME_STEP (30) +/* Define a structure for the configuration variables */ +typedef struct rlm_totp_t { + char const *name; //!< name of this instance */ + uint32_t time_step; //!< seconds + uint32_t otp_length; //!< forced to 6 or 8 + uint32_t lookback_steps; //!< number of steps to look back + uint32_t lookback_interval; //!< interval in seconds between steps +} rlm_totp_t; + +#ifndef TESTING +/* Map configuration file names to internal variables */ +static const CONF_PARSER module_config[] = { + { FR_CONF_OFFSET("time_step", FR_TYPE_UINT32, rlm_totp_t, time_step), .dflt = "30" }, + { FR_CONF_OFFSET("otp_length", FR_TYPE_UINT32, rlm_totp_t, otp_length), .dflt = "6" }, + { FR_CONF_OFFSET("lookback_steps", FR_TYPE_UINT32, rlm_totp_t, lookback_steps), .dflt = "1" }, + { FR_CONF_OFFSET("lookback_interval", FR_TYPE_UINT32, rlm_totp_t, lookback_interval), .dflt = "30" }, + CONF_PARSER_TERMINATOR +}; + +#define TIME_STEP (inst->time_step) +#define OTP_LEN (inst->otp_length) +#define BACK_STEPS (inst->lookback_steps) +#define BACK_STEP_SECS (inst->lookback_interval) +#else +#define TIME_STEP (30) +#define OTP_LEN (8) +#define BACK_STEPS (1) +#define BACK_STEP_SECS (30) +#endif /* * RFC 4648 base32 decoding. @@ -168,13 +196,62 @@ static ssize_t base32_decode(uint8_t *out, size_t outlen, char const *in) } #ifndef TESTING -#define LEN 6 -#define PRINT "%06u" -#define DIV 1000000 -#else -#define LEN 8 -#define PRINT "%08u" -#define DIV 100000000 +# define TESTING_UNUSED +# define LEN 6 +# define PRINT "%06u" +# define DIV 1000000 +#else /* TESTING */ +# define LEN 8 +# define PRINT "%08u" +# define DIV 100000000 +# undef RDEBUG3 +# define RDEBUG3(fmt, ...) printf(fmt "\n", ## __VA_ARGS__) +# define TESTING_UNUSED UNUSED +#endif + +#ifndef TESTING +static int mod_bootstrap(module_inst_ctx_t const *mctx) +{ + rlm_totp_t *inst = talloc_get_type_abort(mctx->inst->data, rlm_totp_t); + CONF_SECTION *conf = mctx->inst->conf; + + inst->name = cf_section_name2(conf); + if (!inst->name) { + inst->name = cf_section_name1(conf); + } + + return 0; +} + +/* + * Do any per-module initialization that is separate to each + * configured instance of the module. e.g. set up connections + * to external databases, read configuration files, set up + * dictionary entries, etc. + * + * If configuration information is given in the config section + * that must be referenced in later calls, store a handle to it + * in *instance otherwise put a null pointer there. + */ +static int mod_instantiate(module_inst_ctx_t const *mctx) +{ + rlm_totp_t *inst = talloc_get_type_abort(mctx->inst->data, rlm_totp_t); + + FR_INTEGER_BOUND_CHECK("time_step", inst->time_step, >=, 5); + FR_INTEGER_BOUND_CHECK("time_step", inst->time_step, <=, 120); + + FR_INTEGER_BOUND_CHECK("lookback_steps", inst->lookback_steps, >=, 1); + FR_INTEGER_BOUND_CHECK("lookback_steps", inst->lookback_steps, <=, 10); + + FR_INTEGER_BOUND_CHECK("lookback_interval", inst->lookback_interval, <=, inst->time_step); + + FR_INTEGER_BOUND_CHECK("otp_length", inst->otp_length, >=, 6); + FR_INTEGER_BOUND_CHECK("otp_length", inst->otp_length, <=, 8); + + if (inst->otp_length == 7) inst->otp_length = 8; + + return 0; +} #endif /* @@ -184,8 +261,10 @@ static ssize_t base32_decode(uint8_t *out, size_t outlen, char const *in) * for 8-character challenges, and not for 6 character * challenges! */ -static int totp_cmp(time_t now, uint8_t const *key, size_t keylen, char const *totp) +static int totp_cmp(TESTING_UNUSED request_t *request, time_t now, uint8_t const *key, size_t keylen, char const *totp, TESTING_UNUSED rlm_totp_t const *inst) { + time_t then; + unsigned int i; uint8_t offset; uint32_t challenge; uint64_t padded; @@ -193,40 +272,56 @@ static int totp_cmp(time_t now, uint8_t const *key, size_t keylen, char const *t uint8_t data[8]; uint8_t digest[SHA1_DIGEST_LENGTH]; - padded = ((uint64_t) now) / TIME_STEP; - data[0] = padded >> 56; - data[1] = padded >> 48; - data[2] = padded >> 40; - data[3] = padded >> 32; - data[4] = padded >> 24; - data[5] = padded >> 16; - data[6] = padded >> 8; - data[7] = padded & 0xff; - /* - * Encrypt the network order time with the key. + * First try to authenticate against the current OTP, then step + * back in increments of BACK_STEP_SECS, up to BACK_STEPS times, + * to authenticate properly in cases of long transit delay, as + * described in RFC 6238, secion 5.2. */ - fr_hmac_sha1(digest, data, 8, key, keylen); - /* - * Take the least significant 4 bits. - */ - offset = digest[SHA1_DIGEST_LENGTH - 1] & 0x0f; + for (i = 0, then = now; i <= BACK_STEPS; i++, then -= BACK_STEP_SECS) { + padded = ((uint64_t) now) / TIME_STEP; + data[0] = padded >> 56; + data[1] = padded >> 48; + data[2] = padded >> 40; + data[3] = padded >> 32; + data[4] = padded >> 24; + data[5] = padded >> 16; + data[6] = padded >> 8; + data[7] = padded & 0xff; - /* - * Grab the 32bits at "offset", and drop the high bit. - */ - challenge = (digest[offset] & 0x7f) << 24; - challenge |= digest[offset + 1] << 16; - challenge |= digest[offset + 2] << 8; - challenge |= digest[offset + 3]; + /* + * Encrypt the network order time with the key. + */ + fr_hmac_sha1(digest, data, 8, key, keylen); - /* - * The token is the last 6 digits in the number. - */ - snprintf(buffer, sizeof(buffer), PRINT, challenge % DIV); + /* + * Take the least significant 4 bits. + */ + offset = digest[SHA1_DIGEST_LENGTH - 1] & 0x0f; + + /* + * Grab the 32bits at "offset", and drop the high bit. + */ + challenge = (digest[offset] & 0x7f) << 24; + challenge |= digest[offset + 1] << 16; + challenge |= digest[offset + 2] << 8; + challenge |= digest[offset + 3]; - return fr_digest_cmp((uint8_t const *) buffer, (uint8_t const *) totp, LEN); + /* + * The token is the last 6 digits in the number (or 8 for testing).. + */ + snprintf(buffer, sizeof(buffer), ((OTP_LEN == 6) ? "%06u" : "%08u"), + challenge % ((OTP_LEN == 6) ? 1000000 : 100000000)); + + RDEBUG3("Now: %zu, Then: %zu", (size_t) now, (size_t) then); + RDEBUG3("Expected %s", buffer); + RDEBUG3("Received %s", totp); + + if (fr_digest_cmp((uint8_t const *) buffer, (uint8_t const *) totp, LEN) == 0) return 0; + } + + return 1; } #ifndef TESTING @@ -237,6 +332,7 @@ static int totp_cmp(time_t now, uint8_t const *key, size_t keylen, char const *t static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request) { rlm_totp_call_env_t *env_data = talloc_get_type_abort(mctx->env_data, rlm_totp_call_env_t); + rlm_totp_t const *instance = talloc_get_type_abort_const(mctx->inst->data, rlm_totp_t); fr_value_box_t *user_password = &env_data->user_password; fr_value_box_t *secret = &env_data->secret; fr_value_box_t *key = &env_data->key; @@ -247,8 +343,13 @@ static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result, if (fr_type_is_null(user_password->type)) RETURN_MODULE_NOOP; - if (user_password->vb_length != 6) { - RDEBUG("TOTP-Password has incorrect length. Expected 6, got %zu", user_password->vb_length); + if (user_password->vb_length == 0) { + RDEBUG("TOTP.From-User is empty"); + RETURN_MODULE_FAIL; + } + + if ((user_password->vb_length != 6) && (user_password->vb_length != 8)) { + RDEBUG("TOTP.From-User has incorrect length. Expected 6 or 8, got %zu", user_password->vb_length); RETURN_MODULE_FAIL; } @@ -274,12 +375,11 @@ static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result, our_keylen = len; } - if (totp_cmp(fr_time_to_sec(request->packet->timestamp), our_key, our_keylen, secret->vb_strvalue) != 0) RETURN_MODULE_FAIL; + if (totp_cmp(request, fr_time_to_sec(request->packet->timestamp), our_key, our_keylen, secret->vb_strvalue, instance) != 0) RETURN_MODULE_FAIL; RETURN_MODULE_OK; } - /* * The module name should be the only globally exported symbol. * That is, everything else should be 'static'. @@ -294,7 +394,10 @@ module_rlm_t rlm_totp = { .common = { .magic = MODULE_MAGIC_INIT, .name = "totp", - .type = MODULE_TYPE_THREAD_SAFE + .type = MODULE_TYPE_THREAD_SAFE, + .config = module_config, + .bootstrap = mod_bootstrap, + .instantiate = mod_instantiate }, .method_names = (module_method_name_t[]){ { .name1 = "authenticate", .name2 = CF_IDENT_ANY, .method = mod_authenticate, .method_env = &method_env }, @@ -335,7 +438,7 @@ int main(int argc, char **argv) (void) sscanf(argv[2], "%llu", &now); - if (totp_cmp((time_t) now, (uint8_t const *) argv[3], strlen(argv[3]), argv[4]) == 0) { + if (totp_cmp(NULL, (time_t) now, (uint8_t const *) argv[3], strlen(argv[3]), argv[4], NULL) == 0) { return 0; } printf("Fail\n"); From 5a8d4f2cb07b2c42a52091e2f44ac7e8e568ff5f Mon Sep 17 00:00:00 2001 From: Jorge Pereira Date: Thu, 14 Sep 2023 18:51:08 -0300 Subject: [PATCH 02/12] rlm_totp: Move 'call_env' to the usual 'attr_dict_autoload' --- src/modules/rlm_totp/rlm_totp.c | 94 +++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/src/modules/rlm_totp/rlm_totp.c b/src/modules/rlm_totp/rlm_totp.c index 9e876bbb5f05..fe206311ba86 100644 --- a/src/modules/rlm_totp/rlm_totp.c +++ b/src/modules/rlm_totp/rlm_totp.c @@ -34,23 +34,33 @@ typedef struct { fr_value_box_t user_password; } rlm_totp_call_env_t; -static const call_env_t call_env[] = { - { FR_CALL_ENV_OFFSET("secret", FR_TYPE_STRING, rlm_totp_call_env_t, secret, - "&control.TOTP.Secret", T_BARE_WORD, false, false, false) }, - - { FR_CALL_ENV_OFFSET("key", FR_TYPE_STRING, rlm_totp_call_env_t, key, - "&control.TOTP.key", T_BARE_WORD, false, false, false) }, +/* Define a structure for the configuration variables */ +typedef struct rlm_totp_t { + char const *name; //!< name of this instance + fr_totp_t totp; //!< Parameter sets for fr_totp_cmp() +} rlm_totp_t; - { FR_CALL_ENV_OFFSET("user_password", FR_TYPE_STRING, rlm_totp_call_env_t, user_password, - "&request.TOTP.From-User", T_BARE_WORD, false, false, false) }, +static fr_dict_t const *dict_freeradius; - CALL_ENV_TERMINATOR +extern fr_dict_autoload_t rlm_totp_dict[]; +fr_dict_autoload_t rlm_totp_dict[] = { + { .out = &dict_freeradius, .proto = "freeradius" }, + { NULL } }; -static const call_method_env_t method_env = { - .inst_size = sizeof(rlm_totp_call_env_t), - .inst_type = "rlm_totp_call_env_t", - .env = call_env +static fr_dict_attr_t const *attr_totp; +static fr_dict_attr_t const *attr_totp_secret; +static fr_dict_attr_t const *attr_totp_key; +static fr_dict_attr_t const *attr_totp_from_user; + +extern fr_dict_attr_autoload_t rlm_totp_dict_attr[]; +fr_dict_attr_autoload_t rlm_totp_dict_attr[] = { + { .out = &attr_totp, .name = "TOTP", .type = FR_TYPE_TLV, .dict = &dict_freeradius }, + { .out = &attr_totp_secret, .name = "TOTP.Secret", .type = FR_TYPE_STRING, .dict = &dict_freeradius }, + { .out = &attr_totp_key, .name = "TOTP.Key", .type = FR_TYPE_OCTETS, .dict = &dict_freeradius }, + { .out = &attr_totp_from_user, .name = "TOTP.From-User", .type = FR_TYPE_STRING, .dict = &dict_freeradius }, + + { NULL } }; /* Define a structure for the configuration variables */ @@ -331,43 +341,46 @@ static int totp_cmp(TESTING_UNUSED request_t *request, time_t now, uint8_t const */ static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request) { - rlm_totp_call_env_t *env_data = talloc_get_type_abort(mctx->env_data, rlm_totp_call_env_t); - rlm_totp_t const *instance = talloc_get_type_abort_const(mctx->inst->data, rlm_totp_t); - fr_value_box_t *user_password = &env_data->user_password; - fr_value_box_t *secret = &env_data->secret; - fr_value_box_t *key = &env_data->key; - - uint8_t const *our_key; - size_t our_keylen; - uint8_t buffer[80]; /* multiple of 5*8 characters */ - - if (fr_type_is_null(user_password->type)) RETURN_MODULE_NOOP; - - if (user_password->vb_length == 0) { - RDEBUG("TOTP.From-User is empty"); - RETURN_MODULE_FAIL; + rlm_totp_t const *inst = talloc_get_type_abort_const(mctx->inst->data, rlm_totp_t); + fr_pair_t *secret, *from_user, *key; + uint8_t const *our_key; + size_t our_keylen; + uint8_t buffer[80]; /* multiple of 5*8 characters */ + fr_dbuff_t out; + + fr_dbuff_init(&out, (uint8_t *)buffer, sizeof(buffer)); + + from_user = fr_pair_find_by_da(&request->request_pairs, NULL, attr_totp_from_user); + if (!from_user) { + REDEBUG("&request.TOTP.From-User is needed"); + RETURN_MODULE_INVALID; } - if ((user_password->vb_length != 6) && (user_password->vb_length != 8)) { - RDEBUG("TOTP.From-User has incorrect length. Expected 6 or 8, got %zu", user_password->vb_length); + if ((from_user->vp_length != 6) && (from_user->vp_length != 8)) { + RDEBUG("&request.TOTP.From-User has incorrect length. Expected 6 or 8, got %zu", from_user->vp_length); RETURN_MODULE_FAIL; } /* - * Look for the raw key first. + * Look for the raw key first in &request.TOTP.Key */ - if (!fr_type_is_null(key->type)) { - our_key = key->vb_octets; - our_keylen = key->vb_length; - + key = fr_pair_find_by_da(&request->control_pairs, NULL, attr_totp_key); + if (key) { + our_key = key->vp_octets; + our_keylen = key->vp_length; } else { ssize_t len; - if (!fr_type_is_null(secret->type)) RETURN_MODULE_NOOP; + /* then let's see &control.TOTP.Secret */ + secret = fr_pair_find_by_da(&request->control_pairs, NULL, attr_totp_secret); + if (!secret) { + REDEBUG("&control.TOTP.Secret is needed"); + RETURN_MODULE_INVALID; + } - len = base32_decode(buffer, sizeof(buffer), secret->vb_strvalue); + len = fr_base32_decode(&out, &FR_SBUFF_IN(secret->vp_strvalue, secret->vp_length), true, true); if (len < 0) { - RDEBUG("TOTP-Secret cannot be decoded"); + RDEBUG("&control.TOTP.Secret cannot be decoded"); RETURN_MODULE_FAIL; } @@ -375,7 +388,7 @@ static unlang_action_t CC_HINT(nonnull) mod_authenticate(rlm_rcode_t *p_result, our_keylen = len; } - if (totp_cmp(request, fr_time_to_sec(request->packet->timestamp), our_key, our_keylen, secret->vb_strvalue, instance) != 0) RETURN_MODULE_FAIL; + if (fr_totp_cmp(&inst->totp, fr_time_to_sec(request->packet->timestamp), our_key, our_keylen, from_user->vp_strvalue) != 0) RETURN_MODULE_FAIL; RETURN_MODULE_OK; } @@ -400,7 +413,8 @@ module_rlm_t rlm_totp = { .instantiate = mod_instantiate }, .method_names = (module_method_name_t[]){ - { .name1 = "authenticate", .name2 = CF_IDENT_ANY, .method = mod_authenticate, .method_env = &method_env }, + { .name1 = "authenticate", .name2 = CF_IDENT_ANY, .method = mod_authenticate }, + { .name1 = CF_IDENT_ANY, .name2 = CF_IDENT_ANY, .method = mod_authenticate }, MODULE_NAME_TERMINATOR } }; From f5c2f5b4fefc04450728cbc689e6fdb135f687ba Mon Sep 17 00:00:00 2001 From: Jorge Pereira Date: Thu, 14 Sep 2023 22:50:45 -0300 Subject: [PATCH 03/12] totp: improve the test sample --- .../rlm_totp => tests/totp/bin}/.gitignore | 0 .../rlm_totp => tests/totp/bin}/Makefile | 13 +- .../rlm_totp => tests/totp/bin}/sha1.txt | 0 src/tests/totp/bin/totp.c | 124 ++++++++++++++++++ 4 files changed, 132 insertions(+), 5 deletions(-) rename src/{modules/rlm_totp => tests/totp/bin}/.gitignore (100%) rename src/{modules/rlm_totp => tests/totp/bin}/Makefile (59%) rename src/{modules/rlm_totp => tests/totp/bin}/sha1.txt (100%) create mode 100644 src/tests/totp/bin/totp.c diff --git a/src/modules/rlm_totp/.gitignore b/src/tests/totp/bin/.gitignore similarity index 100% rename from src/modules/rlm_totp/.gitignore rename to src/tests/totp/bin/.gitignore diff --git a/src/modules/rlm_totp/Makefile b/src/tests/totp/bin/Makefile similarity index 59% rename from src/modules/rlm_totp/Makefile rename to src/tests/totp/bin/Makefile index fbe24303db7b..7f489a4c4b52 100644 --- a/src/modules/rlm_totp/Makefile +++ b/src/tests/totp/bin/Makefile @@ -3,26 +3,29 @@ # all: totp -include ../../../Make.inc +include ../../../../Make.inc # # Hack up stuff so we can build in a subdirectory. # .PHONY: src src: - @ln -sf ../../../src + @ln -sf ../../../../src .PHONY: freeradius-devel freeradius-devel: - @ln -sf ../../../src/include freeradius-devel + @ln -sf ../../../../src/include freeradius-devel # # ./totp decode # # ./totp totp