diff options
Diffstat (limited to 'src/modules/rlm_totp')
-rw-r--r-- | src/modules/rlm_totp/.gitignore | 3 | ||||
-rw-r--r-- | src/modules/rlm_totp/Makefile | 33 | ||||
-rw-r--r-- | src/modules/rlm_totp/README.md | 9 | ||||
-rw-r--r-- | src/modules/rlm_totp/all.mk | 2 | ||||
-rw-r--r-- | src/modules/rlm_totp/rlm_totp.c | 320 | ||||
-rw-r--r-- | src/modules/rlm_totp/sha1.txt | 6 |
6 files changed, 373 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/README.md b/src/modules/rlm_totp/README.md new file mode 100644 index 0000000..2dd5711 --- /dev/null +++ b/src/modules/rlm_totp/README.md @@ -0,0 +1,9 @@ +# rlm_totp +## Metadata +<dl> + <dt>category</dt><dd>authentication</dd> +</dl> + +## Summary + +Implement the TOTP algorithm as given in RFC 6238. 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 |