summaryrefslogtreecommitdiffstats
path: root/src/modules/rlm_totp
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules/rlm_totp')
-rw-r--r--src/modules/rlm_totp/Makefile2
-rw-r--r--src/modules/rlm_totp/rlm_totp.c393
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