]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
Port new features and documentation from v3.2
authorJorge Pereira <jpereira@freeradius.org>
Wed, 6 Sep 2023 20:40:09 +0000 (17:40 -0300)
committerAlan T. DeKok <aland@freeradius.org>
Wed, 27 Sep 2023 11:40:25 +0000 (07:40 -0400)
raddb/mods-available/totp
src/modules/rlm_totp/rlm_totp.c

index b354122b64ec287a9fb18a0e48362a12fe410926..43d2f57afa428d6b242e5d54104f04a648eda6b1 100644 (file)
 #  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
 }
index 8c283272defb866ade25affc3acd7f2527d63584..0abb3abae52ae2bcbc036cc52365d71c57afcd5a 100644 (file)
@@ -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,58 @@ 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
+#else /* TESTING */
+# define LEN 8
+# 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 +257,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 +268,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 +328,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 +339,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 +371,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 +390,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 +434,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");