diff options
Diffstat (limited to 'src/modules/rlm_totp')
-rw-r--r-- | src/modules/rlm_totp/Makefile | 2 | ||||
-rw-r--r-- | src/modules/rlm_totp/rlm_totp.c | 393 |
2 files changed, 345 insertions, 50 deletions
diff --git a/src/modules/rlm_totp/Makefile b/src/modules/rlm_totp/Makefile index 5ad2ae9..595cea6 100644 --- a/src/modules/rlm_totp/Makefile +++ b/src/modules/rlm_totp/Makefile @@ -22,7 +22,7 @@ freeradius-devel: # ./totp totp <time> <sha1key> <8-character-challenge> # totp: rlm_totp.c | src freeradius-devel - @$(CC) -DTESTING $(CFLAGS) $(OPENSSL_CPPFLAGS) -o $@ $(LDFLAGS) $(LIBS) ../../../build/lib/.libs/libfreeradius-radius.a rlm_totp.c + @$(CC) -DTESTING $(CFLAGS) $(CPPFLAGS) $(OPENSSL_CPPFLAGS) -o $@ $(LDFLAGS) $(LIBS) ../../../build/lib/.libs/libfreeradius-radius.a rlm_totp.c # # Test vectors from RFC 6238, Appendix B diff --git a/src/modules/rlm_totp/rlm_totp.c b/src/modules/rlm_totp/rlm_totp.c index 1459716..d58e1ee 100644 --- a/src/modules/rlm_totp/rlm_totp.c +++ b/src/modules/rlm_totp/rlm_totp.c @@ -26,9 +26,68 @@ RCSID("$Id$") #include <freeradius-devel/radiusd.h> #include <freeradius-devel/modules.h> +#include <freeradius-devel/dlist.h> #include <freeradius-devel/rad_assert.h> -#define TIME_STEP (30) +#include <ctype.h> + +typedef struct { + uint8_t const *key; + size_t keylen; + char const *passwd; + time_t when; + bool unlisted; + void *instance; + fr_dlist_t dlist; +} totp_dedup_t; + +#ifdef HAVE_PTHREAD_H +#include <pthread.h> + +#define PTHREAD_MUTEX_LOCK(_x) pthread_mutex_lock(&((_x)->mutex)) +#define PTHREAD_MUTEX_UNLOCK(_x) pthread_mutex_unlock(&((_x)->mutex)) +#else +#define PTHREAD_MUTEX_LOCK(_x) +#define PTHREAD_MUTEX_UNLOCK(_x) +#endif + + +/* 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 + uint32_t lookforward_steps; //!< number of steps to look forwards + rbtree_t *dedup_tree; + fr_dlist_t dedup_list; +#ifdef HAVE_PTHREAD_H + pthread_mutex_t mutex; +#endif +} rlm_totp_t; + +#ifndef TESTING +/* Map configuration file names to internal variables */ +static const CONF_PARSER module_config[] = { + { "time_step", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_totp_t, time_step), "30" }, + { "otp_length", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_totp_t, otp_length), "6" }, + { "lookback_steps", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_totp_t, lookback_steps), "1" }, + { "lookback_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_totp_t, lookback_interval), "30" }, + { "lookforward_steps", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_totp_t, lookforward_steps), "0" }, + CONF_PARSER_TERMINATOR +}; + +#define TIME_STEP (inst->time_step) +#define OTP_LEN (inst->otp_length) +#define BACK_STEPS (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. @@ -110,7 +169,7 @@ static ssize_t base32_decode(uint8_t *out, size_t outlen, char const *in) * Will get converted to * * 11111222 22333334 44445555 56666677 77788888 - */ + */ for (p = b = out; p < end; p += 8) { b[0] = p[0] << 3; b[0] |= p[1] >> 2; @@ -142,14 +201,112 @@ static ssize_t base32_decode(uint8_t *out, size_t outlen, char const *in) return b - out; } + #ifndef TESTING -#define LEN 6 -#define PRINT "%06u" -#define DIV 1000000 -#else -#define LEN 8 -#define PRINT "%08u" -#define DIV 100000000 +#define TESTING_UNUSED + +#else /* TESTING */ +#undef RDEBUG3 +#define RDEBUG3(fmt, ...) printf(fmt "\n", ## __VA_ARGS__) +#define TESTING_UNUSED UNUSED +#endif + +#ifndef TESTING +static int mod_bootstrap(CONF_SECTION *conf, void *instance) +{ + rlm_totp_t *inst = instance; + + inst->name = cf_section_name2(conf); + if (!inst->name) { + inst->name = cf_section_name1(conf); + } + + return 0; +} + +static int dedup_cmp(void const *one, void const *two) +{ + int rcode; + totp_dedup_t const *a = one; + totp_dedup_t const *b = two; + + if (a->keylen < b->keylen) return -1; + if (a->keylen > b->keylen) return +1; + + rcode = memcmp(a->key , b->key, a->keylen); + if (rcode != 0) return rcode; + + /* + * The user can enter multiple keys + */ + return strcmp(a->passwd, b->passwd); +} + +static void dedup_free(void *data) +{ + totp_dedup_t *dedup = data; +#ifdef HAVE_PTHREAD_H + rlm_totp_t *inst = dedup->instance; +#endif + + if (!dedup->unlisted) { + PTHREAD_MUTEX_LOCK(inst); + fr_dlist_entry_unlink(&dedup->dlist); + PTHREAD_MUTEX_UNLOCK(inst); + } + + free(dedup); +} + +/* + * 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(UNUSED CONF_SECTION *conf, void *instance) +{ + rlm_totp_t *inst = instance; + + 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("lookforward_steps", inst->lookforward_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; + + inst->dedup_tree = rbtree_create(instance, dedup_cmp, dedup_free, 0); + if (!inst->dedup_tree) return -1; + + fr_dlist_entry_init(&inst->dedup_list); +#ifdef HAVE_PTHREAD_H + (void) pthread_mutex_init(&inst->mutex, NULL); +#endif + + return 0; +} + +#ifdef HAVE_PTHREAD_H +static int mod_detach(void *instance) +{ + rlm_totp_t *inst = instance; + + pthread_mutex_destroy(&inst->mutex); + return 0; +} +#endif #endif /* @@ -159,8 +316,14 @@ 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 *request, time_t now, uint8_t const *key, size_t keylen, char const *totp, TESTING_UNUSED void *instance) { +#ifndef TESTING + rlm_totp_t *inst = instance; + uint32_t steps = inst->lookback_steps > inst->lookforward_steps ? inst->lookback_steps : inst->lookforward_steps; +#endif + time_t diff, then; + unsigned int i; uint8_t offset; uint32_t challenge; uint64_t padded; @@ -168,59 +331,153 @@ 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, diff = 0; i <= BACK_STEPS; i++, diff += BACK_STEP_SECS) { +#ifndef TESTING + if (i > inst->lookback_steps) goto forwards; +#endif + then = now - diff; +#ifndef TESTING + repeat: +#endif + padded = (uint64_t) then / 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. + */ + fr_hmac_sha1(digest, data, 8, key, keylen); + + /* + * 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]; + + /* + * 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 (rad_digest_cmp((uint8_t const *) buffer, (uint8_t const *) totp, OTP_LEN) == 0) return 0; + +#ifndef TESTING + /* + * We've tested backwards, now do the equivalent time slot forwards + */ + if ((then < now) && (i <= inst->lookforward_steps)) { + forwards: + then = now + diff; + goto repeat; + } +#endif + } + return 1; +} + +#ifndef TESTING + +static inline CC_HINT(nonnull) totp_dedup_t *fr_dlist_head(fr_dlist_t const *head) +{ + if (head->prev == head) return NULL; + + return (totp_dedup_t *) (((uintptr_t) head->next) - offsetof(totp_dedup_t, dlist)); +} + + +static bool totp_reused(void *instance, time_t now, uint8_t const *key, size_t keylen, char const *passwd) +{ + rlm_totp_t *inst = instance; + totp_dedup_t *dedup, my_dedup; + + my_dedup.key = key; + my_dedup.keylen = keylen; + my_dedup.passwd = passwd; + + PTHREAD_MUTEX_LOCK(inst); /* - * Grab the 32bits at "offset", and drop the high bit. + * Expire the oldest entries before searching for an entry in the tree. */ - challenge = (digest[offset] & 0x7f) << 24; - challenge |= digest[offset + 1] << 16; - challenge |= digest[offset + 2] << 8; - challenge |= digest[offset + 3]; + while (true) { + dedup = fr_dlist_head(&inst->dedup_list); + if (!dedup) break; + + if ((now - dedup->when) < (inst->lookback_steps * inst->lookback_interval)) break; + + dedup->unlisted = true; + fr_dlist_entry_unlink(&dedup->dlist); + (void) rbtree_deletebydata(inst->dedup_tree, dedup); + } /* - * The token is the last 6 digits in the number. + * Was this key and TOTP reused? */ - snprintf(buffer, sizeof(buffer), PRINT, challenge % DIV); + dedup = rbtree_finddata(inst->dedup_tree, &my_dedup); + if (dedup) { + PTHREAD_MUTEX_UNLOCK(inst); + return true; + } - return rad_digest_cmp((uint8_t const *) buffer, (uint8_t const *) totp, LEN); -} + dedup = calloc(sizeof(*dedup), 1); + if (!dedup) { + PTHREAD_MUTEX_UNLOCK(inst); + return false; + } -#ifndef TESTING + dedup->key = key; + dedup->keylen = keylen; + dedup->passwd = passwd; + dedup->when = now; + dedup->instance = inst; + + fr_dlist_insert_tail(&inst->dedup_list, &dedup->dlist); + (void) rbtree_insert(inst->dedup_tree, dedup); + PTHREAD_MUTEX_UNLOCK(inst); + + return false; +} /* * Do the authentication */ -static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(UNUSED void *instance, REQUEST *request) +static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(void *instance, REQUEST *request) { VALUE_PAIR *vp, *password; uint8_t const *key; size_t keylen; uint8_t buffer[80]; /* multiple of 5*8 characters */ - + uint64_t now = time(NULL); password = fr_pair_find_by_num(request->packet->vps, PW_TOTP_PASSWORD, 0, TAG_ANY); if (!password) return RLM_MODULE_NOOP; - if (password->vp_length != 6) { + if ((password->vp_length != 6) && (password->vp_length != 8)) { RDEBUG("TOTP-Password has incorrect length %d", (int) password->vp_length); return RLM_MODULE_FAIL; } @@ -237,11 +494,13 @@ static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(UNUSED void *instance, REQU ssize_t len; vp = fr_pair_find_by_num(request->config, PW_TOTP_SECRET, 0, TAG_ANY); - if (!vp) return RLM_MODULE_NOOP; - + if (!vp) { + RDEBUG("TOTP mod_authenticate() did not receive a TOTP-Secret"); + return RLM_MODULE_NOOP; + } len = base32_decode(buffer, sizeof(buffer), vp->vp_strvalue); if (len < 0) { - RDEBUG("TOTP-Secret cannot be decoded"); + REDEBUG("TOTP-Secret cannot be decoded"); return RLM_MODULE_FAIL; } @@ -249,9 +508,25 @@ static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(UNUSED void *instance, REQU keylen = len; } - if (totp_cmp(time(NULL), key, keylen, password->vp_strvalue) != 0) return RLM_MODULE_FAIL; + vp = fr_pair_find_by_num(request->config, PW_TOTP_TIME_OFFSET, 0, TAG_ANY); + if (vp && (vp->vp_signed > -600) && (vp->vp_signed < 600)) { + RDEBUG("Using TOTP-Time-Offset = %d", vp->vp_signed); + now += vp->vp_signed; + } + + if (totp_cmp(request, now, key, keylen, password->vp_strvalue, instance) == 0) { + /* + * Forbid using a key more than once. + */ + if (totp_reused(instance, now, key, keylen, password->vp_strvalue)) return RLM_MODULE_REJECT; + + return RLM_MODULE_OK; + } - return RLM_MODULE_OK; + /* + * Bad keys don't affect the cache. + */ + return RLM_MODULE_REJECT; } @@ -269,12 +544,24 @@ module_t rlm_totp = { .magic = RLM_MODULE_INIT, .name = "totp", .type = RLM_TYPE_THREAD_SAFE, + .inst_size = sizeof(rlm_totp_t), + .config = module_config, + .bootstrap = mod_bootstrap, + .instantiate = mod_instantiate, +#ifdef HAVE_PTHREAD_H + .detach = mod_detach, +#endif .methods = { [MOD_AUTHENTICATE] = mod_authenticate, }, }; #else /* TESTING */ +/* + * ./totp decode KEY_BASE32 + * + * ./totp totp now KEY TOTP + */ int main(int argc, char **argv) { size_t len; @@ -298,23 +585,31 @@ int main(int argc, char **argv) } /* - * TOTP <time> <key> <8-character-expected-token> + * TOTP <time> <key> <expected-token> */ if (strcmp(argv[1], "totp") == 0) { uint64_t now; if (argc < 5) return 0; - (void) sscanf(argv[2], "%llu", &now); + if (strcmp(argv[2], "now") == 0) { + now = time(NULL); + } else { + (void) sscanf(argv[2], "%llu", &now); + } + + printf ("=== Time = %llu, TIME_STEP = %d, BACK_STEPS = %d, BACK_STEP_SECS = %d ===\n", + now, TIME_STEP, BACK_STEPS, BACK_STEP_SECS); - if (totp_cmp((time_t) now, (uint8_t const *) argv[3], strlen(argv[3]), argv[4]) == 0) { - return 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"); return 1; } - fprintf(stderr, "Unknown command argv[1]\n", argv[1]); + fprintf(stderr, "Unknown command %s\n", argv[1]); return 1; } #endif |