summaryrefslogtreecommitdiffstats
path: root/src/modules/rlm_totp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/modules/rlm_totp/.gitignore3
-rw-r--r--src/modules/rlm_totp/Makefile33
-rw-r--r--src/modules/rlm_totp/all.mk2
-rw-r--r--src/modules/rlm_totp/rlm_totp.c320
-rw-r--r--src/modules/rlm_totp/sha1.txt6
5 files changed, 364 insertions, 0 deletions
diff --git a/src/modules/rlm_totp/.gitignore b/src/modules/rlm_totp/.gitignore
new file mode 100644
index 0000000..fb9b99c
--- /dev/null
+++ b/src/modules/rlm_totp/.gitignore
@@ -0,0 +1,3 @@
+totp
+src
+freeradius-devel
diff --git a/src/modules/rlm_totp/Makefile b/src/modules/rlm_totp/Makefile
new file mode 100644
index 0000000..5ad2ae9
--- /dev/null
+++ b/src/modules/rlm_totp/Makefile
@@ -0,0 +1,33 @@
+#
+# TOTP isn't simple, so we need test cases.
+#
+all: totp
+
+include ../../../Make.inc
+
+#
+# Hack up stuff so we can build in a subdirectory.
+#
+.PHONY: src
+src:
+ @ln -sf ../../../src
+
+.PHONY: freeradius-devel
+freeradius-devel:
+ @ln -sf ../../../src/include freeradius-devel
+
+#
+# ./totp decode <base32>
+#
+# ./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
+
+#
+# Test vectors from RFC 6238, Appendix B
+#
+test: totp
+ @while IFS= read -r line; do \
+ ./totp totp $$line || exit 1; \
+ done < sha1.txt
diff --git a/src/modules/rlm_totp/all.mk b/src/modules/rlm_totp/all.mk
new file mode 100644
index 0000000..4f58d0e
--- /dev/null
+++ b/src/modules/rlm_totp/all.mk
@@ -0,0 +1,2 @@
+TARGET := rlm_totp.a
+SOURCES := rlm_totp.c
diff --git a/src/modules/rlm_totp/rlm_totp.c b/src/modules/rlm_totp/rlm_totp.c
new file mode 100644
index 0000000..1459716
--- /dev/null
+++ b/src/modules/rlm_totp/rlm_totp.c
@@ -0,0 +1,320 @@
+/*
+ * This program is is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ * @file rlm_totp.c
+ * @brief Execute commands and parse the results.
+ *
+ * @copyright 2021 The FreeRADIUS server project
+ * @copyright 2021 Network RADIUS SARL (legal@networkradius.com)
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/rad_assert.h>
+
+#define TIME_STEP (30)
+
+/*
+ * RFC 4648 base32 decoding.
+ */
+static const uint8_t alphabet[UINT8_MAX] = {
+ ['A'] = 1,
+ ['B'] = 2,
+ ['C'] = 3,
+ ['D'] = 4,
+ ['E'] = 5,
+ ['F'] = 6,
+ ['G'] = 7,
+ ['H'] = 8,
+ ['I'] = 9,
+ ['J'] = 10,
+ ['K'] = 11,
+ ['L'] = 12,
+ ['M'] = 13,
+ ['N'] = 14,
+ ['O'] = 15,
+ ['P'] = 16,
+ ['Q'] = 17,
+ ['R'] = 18,
+ ['S'] = 19,
+ ['T'] = 20,
+ ['U'] = 21,
+ ['V'] = 22,
+ ['W'] = 23,
+ ['X'] = 24,
+ ['Y'] = 25,
+ ['Z'] = 26,
+ ['2'] = 27,
+ ['3'] = 28,
+ ['4'] = 29,
+ ['5'] = 30,
+ ['6'] = 31,
+ ['7'] = 32,
+};
+
+static ssize_t base32_decode(uint8_t *out, size_t outlen, char const *in)
+{
+ uint8_t *p, *end, *b;
+ char const *q;
+
+ p = out;
+ end = p + outlen;
+
+ memset(out, 0, outlen);
+
+ /*
+ * Convert ASCII to binary.
+ */
+ for (q = in; *q != '\0'; q++) {
+ /*
+ * Padding at the end, stop.
+ */
+ if (*q == '=') {
+ break;
+ }
+
+ if (!alphabet[*((uint8_t const *) q)]) return -1;
+
+ *(p++) = alphabet[*((uint8_t const *) q)] - 1;
+
+ if (p == end) return -1; /* too much data */
+ }
+
+ /*
+ * Reset to the end of the actual data we have
+ */
+ end = p;
+
+ /*
+ * Convert input 5-bit groups into output 8-bit groups.
+ * We do this in 8-byte blocks.
+ *
+ * 00011111 00022222 00033333 00044444 00055555 00066666 00077777 00088888
+ *
+ * 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;
+
+ b[1] = p[1] << 6;
+ b[1] |= p[2] << 1;
+ b[1] |= p[3] >> 4;
+
+ b[2] = p[3] << 4;
+ b[2] |= p[4] >> 1;
+
+ b[3] = p[4] << 7;
+ b[3] |= p[5] << 2;
+ b[3] |= p[6] >> 3;
+
+ b[4] = p[6] << 5;
+ b[4] |= p[7];
+
+ b += 5;
+
+ /*
+ * Clear out the remaining 3 octets of this block.
+ */
+ b[0] = 0;
+ b[1] = 0;
+ b[2] = 0;
+ }
+
+ return b - out;
+}
+
+#ifndef TESTING
+#define LEN 6
+#define PRINT "%06u"
+#define DIV 1000000
+#else
+#define LEN 8
+#define PRINT "%08u"
+#define DIV 100000000
+#endif
+
+/*
+ * Implement RFC 6238 TOTP algorithm.
+ *
+ * Appendix B has test vectors. Note that the test vectors are
+ * 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)
+{
+ uint8_t offset;
+ uint32_t challenge;
+ uint64_t padded;
+ char buffer[9];
+ 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.
+ */
+ 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.
+ */
+ snprintf(buffer, sizeof(buffer), PRINT, challenge % DIV);
+
+ return rad_digest_cmp((uint8_t const *) buffer, (uint8_t const *) totp, LEN);
+}
+
+#ifndef TESTING
+
+/*
+ * Do the authentication
+ */
+static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(UNUSED void *instance, REQUEST *request)
+{
+ VALUE_PAIR *vp, *password;
+ uint8_t const *key;
+ size_t keylen;
+ uint8_t buffer[80]; /* multiple of 5*8 characters */
+
+
+ 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) {
+ RDEBUG("TOTP-Password has incorrect length %d", (int) password->vp_length);
+ return RLM_MODULE_FAIL;
+ }
+
+ /*
+ * Look for the raw key first.
+ */
+ vp = fr_pair_find_by_num(request->config, PW_TOTP_KEY, 0, TAG_ANY);
+ if (vp) {
+ key = vp->vp_octets;
+ keylen = vp->vp_length;
+
+ } else {
+ ssize_t len;
+
+ vp = fr_pair_find_by_num(request->config, PW_TOTP_SECRET, 0, TAG_ANY);
+ if (!vp) return RLM_MODULE_NOOP;
+
+ len = base32_decode(buffer, sizeof(buffer), vp->vp_strvalue);
+ if (len < 0) {
+ RDEBUG("TOTP-Secret cannot be decoded");
+ return RLM_MODULE_FAIL;
+ }
+
+ key = buffer;
+ keylen = len;
+ }
+
+ if (totp_cmp(time(NULL), key, keylen, password->vp_strvalue) != 0) return RLM_MODULE_FAIL;
+
+ return RLM_MODULE_OK;
+}
+
+
+/*
+ * The module name should be the only globally exported symbol.
+ * That is, everything else should be 'static'.
+ *
+ * If the module needs to temporarily modify it's instantiation
+ * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
+ * The server will then take care of ensuring that the module
+ * is single-threaded.
+ */
+extern module_t rlm_totp;
+module_t rlm_totp = {
+ .magic = RLM_MODULE_INIT,
+ .name = "totp",
+ .type = RLM_TYPE_THREAD_SAFE,
+ .methods = {
+ [MOD_AUTHENTICATE] = mod_authenticate,
+ },
+};
+
+#else /* TESTING */
+int main(int argc, char **argv)
+{
+ size_t len;
+ uint8_t *p;
+ uint8_t key[80];
+
+ if (argc < 2) return 0;
+
+ if (strcmp(argv[1], "decode") == 0) {
+ if (argc < 3) return 0;
+
+ len = base32_decode(key, sizeof(key), argv[2]);
+ printf("Decoded %ld %s\n", len, key);
+
+ for (p = key; p < (key + len); p++) {
+ printf("%02x ", *p);
+ };
+ printf("\n");
+
+ return 0;
+ }
+
+ /*
+ * TOTP <time> <key> <8-character-expected-token>
+ */
+ if (strcmp(argv[1], "totp") == 0) {
+ uint64_t now;
+
+ if (argc < 5) return 0;
+
+ (void) sscanf(argv[2], "%llu", &now);
+
+ if (totp_cmp((time_t) now, (uint8_t const *) argv[3], strlen(argv[3]), argv[4]) == 0) {
+ return 0;
+ }
+ printf("Fail\n");
+ return 1;
+ }
+
+ fprintf(stderr, "Unknown command argv[1]\n", argv[1]);
+ return 1;
+}
+#endif
diff --git a/src/modules/rlm_totp/sha1.txt b/src/modules/rlm_totp/sha1.txt
new file mode 100644
index 0000000..da85ab1
--- /dev/null
+++ b/src/modules/rlm_totp/sha1.txt
@@ -0,0 +1,6 @@
+59 12345678901234567890 94287082
+1111111109 12345678901234567890 07081804
+1111111111 12345678901234567890 14050471
+1234567890 12345678901234567890 89005924
+2000000000 12345678901234567890 69279037
+20000000000 12345678901234567890 65353130