diff --git a/debian/libfreeradius4.install b/debian/libfreeradius4.install index 44754cb059c10..9d2aa78d1bc47 100644 --- a/debian/libfreeradius4.install +++ b/debian/libfreeradius4.install @@ -14,6 +14,7 @@ usr/lib/freeradius/libfreeradius-sim.so usr/lib/freeradius/libfreeradius-tacacs.so usr/lib/freeradius/libfreeradius-tftp.so usr/lib/freeradius/libfreeradius-tls.so +usr/lib/freeradius/libfreeradius-totp.so usr/lib/freeradius/libfreeradius-unlang.so usr/lib/freeradius/libfreeradius-util.so usr/lib/freeradius/libfreeradius-vmps.so diff --git a/doc/antora/modules/raddb/nav.adoc b/doc/antora/modules/raddb/nav.adoc index 1e7cea6416707..91c9b5b095476 100644 --- a/doc/antora/modules/raddb/nav.adoc +++ b/doc/antora/modules/raddb/nav.adoc @@ -67,6 +67,7 @@ *** xref:mods-available/sqlippool.adoc[SQL-IP-Pool Module] *** xref:mods-available/sradutmp.adoc[sRadutmp Module] *** xref:mods-available/stats.adoc[Stats Module] +*** xref:mods-available/totp.adoc[TOTP Module] *** xref:mods-available/unbound.adoc[Unbound Module] *** xref:mods-available/unix.adoc[Unix Module] *** xref:mods-available/unpack.adoc[Unpack Module] diff --git a/doc/antora/modules/raddb/pages/mods-available/totp.adoc b/doc/antora/modules/raddb/pages/mods-available/totp.adoc index cbb91248810ac..075733bdfbcf3 100644 --- a/doc/antora/modules/raddb/pages/mods-available/totp.adoc +++ b/doc/antora/modules/raddb/pages/mods-available/totp.adoc @@ -1,26 +1,33 @@ -Time-based One-Time Passwords (TOTP) += Time-based One-Time Passwords (TOTP) -Defined in https://tools.ietf.org/html/rfc6238[RFC 6238], and used in Google Authenticator. +Defined in `rfc6238`, and used in Google Authenticator. This module can only be used in the "authenticate" section. The Base32-encoded secret should be placed into: +Any "bare" key should be placed into: + + +If `TOTP.Key` exists, then it will be used instead of `TOTP.Secret`. + The TOTP password entered by the user should be placed into: -The module will return "ok" if the passwords match, and "fail" +The module will return `ok` if the passwords match, and `fail` if the passwords do not match. -Note that this module will NOT interact with Google. The module is +NOTE: The crypto algorithms are HmacSHA1, HmacSHA256 and HmacSHA512. + +NOTE: This module will *NOT* interact with Google. The module is intended to be used where the local administrator knows the TOTP secret key, and user has an authenticator app on their phone. -Note also that while you can use the Google "chart" APIs to +NOTE: Also that while you can use the Google "chart" APIs to generate a QR code, doing this will give the secret to Google! Administrators should instead install a tool such as "qrcode" @@ -29,15 +36,42 @@ Administrators should instead install a tool such as "qrcode" and then run that locally to get an image. -The module takes no configuration items. + +## Configuration Settings + +totp { ... }:: + + +time_step:: Default time step between time changes. + + + +otp_length:: Length of the one-time password. + +Must be 6 or 8 + + + +lookback_steps:: How many steps backward in time we look for a matching OTP. + + + +lookback_interval:: Time delta between steps. + +Cannot be larger than `time_step` == Default Configuration ``` -# &control.TOTP.Secret -# &request.TOTP.From-User +# `&control.TOTP.Secret` +# `&control.TOTP.Key` +# `&request.TOTP.From-User` # https://linux.die.net/man/1/qrencode totp { + time_step = 30 + otp_length = 6 + lookback_steps = 1 + lookback_interval = 30 } ``` diff --git a/raddb/mods-available/totp b/raddb/mods-available/totp index b354122b64ec2..22eabe8dc370e 100644 --- a/raddb/mods-available/totp +++ b/raddb/mods-available/totp @@ -1,36 +1,39 @@ -# -*- text -*- +# -*- text -*- # # $Id$ +######################################################################## # -# Time-based One-Time Passwords (TOTP) +# = Time-based One-Time Passwords (TOTP) # -# Defined in RFC 6238, and used in Google Authenticator. +# Defined in `rfc6238`, and used in Google Authenticator. # # This module can only be used in the "authenticate" section. # # The Base32-encoded secret should be placed into: # -# &control:TOTP-Secret +# `&control.TOTP.Secret` # # Any "bare" key should be placed into: # -# &control:TOTP-Key +# `&control.TOTP.Key` # -# If TOTP-Key exists, then it will be used instead of TOTP-Secret. +# If `TOTP.Key` exists, then it will be used instead of `TOTP.Secret`. # # The TOTP password entered by the user should be placed into: # -# &request:TOTP-Password +# `&request.TOTP.From-User` # -# The module will return "ok" if the passwords match, and "fail" +# The module will return `ok` if the passwords match, and `fail` # if the passwords do not match. # -# Note that this module will NOT interact with Google. The module is +# NOTE: The crypto algorithms are HmacSHA1, HmacSHA256 and HmacSHA512. +# +# NOTE: This module will *NOT* interact with Google. The module is # intended to be used where the local administrator knows the TOTP # secret key, and user has an authenticator app on their phone. # -# Note also that while you can use the Google "chart" APIs to +# NOTE: Also that while you can use the Google "chart" APIs to # generate a QR code, doing this will give the secret to Google! # # Administrators should instead install a tool such as "qrcode" @@ -38,9 +41,35 @@ # https://linux.die.net/man/1/qrencode # # and then run that locally to get an image. -# # -# The module takes no configuration items. + +# +# ## Configuration Settings +# +# totp { ... }:: # totp { + # + # time_step:: Default time step between time changes. + # + time_step = 30 + + # + # otp_length:: Length of the one-time password. + # + # Must be 6 or 8 + # + otp_length = 6 + + # + # lookback_steps:: How many steps backward in time we look for a matching OTP. + # + lookback_steps = 1 + + # + # lookback_interval:: Time delta between steps. + # + # Cannot be larger than `time_step` + # + lookback_interval = 30 } diff --git a/redhat/freeradius.spec b/redhat/freeradius.spec index 05ff79eb41839..a783fe7717f6d 100644 --- a/redhat/freeradius.spec +++ b/redhat/freeradius.spec @@ -946,6 +946,7 @@ fi %{_libdir}/freeradius/libfreeradius-tacacs.so %{_libdir}/freeradius/libfreeradius-tftp.so %{_libdir}/freeradius/libfreeradius-tls.so +%{_libdir}/freeradius/libfreeradius-totp.so %{_libdir}/freeradius/libfreeradius-unlang.so %{_libdir}/freeradius/libfreeradius-vmps.so diff --git a/scripts/totp/totp-gen.py b/scripts/totp/totp-gen.py new file mode 100755 index 0000000000000..bd3b65b8faed7 --- /dev/null +++ b/scripts/totp/totp-gen.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# Author: Jorge Pereira +# Simple TOTP generator. + +import argparse +import base64 +import hmac +import struct +import sys +import time + +def hotp(key, counter, digits=6, digest='sha1'): + key = key.encode('ascii') + counter = struct.pack('>Q', counter) + mac = hmac.new(key, counter, digest).digest() + offset = mac[-1] & 0x0f + binary = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff + return str(binary)[-digits:].zfill(digits) + +def totp(key, time_step=30, digits=6, digest='sha1'): + return hotp(key, int(time.time() / time_step), digits, digest) + +def main(): + parser = argparse.ArgumentParser(description = "Simple TOTP token generator.") + parser.add_argument("-k", dest = "key", help = "Key in raw format.", required = True) + parser.add_argument("-t", dest = "time_step", help = "time step between time changes.", default = 30, type = int) + parser.add_argument("-d", dest = "digits", help = "Length of the one-time password.", default = 6, type = int) + parser.add_argument("-e", dest = "encode_base32", help = "Encode the output token in base32.", action='store_true') + parser.add_argument("-D", dest = "digest", help = "HMAC algorithm as described by RFC 2104. default: sha1, options: sha1, sha256, sha512", required = False, default = "sha1") + args = parser.parse_args() + token = totp(args.key, args.time_step, args.digits, args.digest) + + if args.encode_base32: + token = base64.b32encode(bytearray(token, 'ascii')).decode('ascii') + + print(token) + +if __name__ == '__main__': + main() diff --git a/share/dictionary/freeradius/dictionary b/share/dictionary/freeradius/dictionary index f842dd3588571..49233c3be92dd 100644 --- a/share/dictionary/freeradius/dictionary +++ b/share/dictionary/freeradius/dictionary @@ -22,6 +22,9 @@ $INCLUDE dictionary.freeradius.internal.ippool # 5200-5299 SIM management attributes $INCLUDE dictionary.freeradius.internal.sim +# 5300-5399 TOTP related attributes +$INCLUDE dictionary.freeradius.internal.totp + # # Include module-specific dictionaries. # diff --git a/share/dictionary/freeradius/dictionary.freeradius.internal.password b/share/dictionary/freeradius/dictionary.freeradius.internal.password index 92c907a585bf4..be20463ded35b 100644 --- a/share/dictionary/freeradius/dictionary.freeradius.internal.password +++ b/share/dictionary/freeradius/dictionary.freeradius.internal.password @@ -64,13 +64,4 @@ ATTRIBUTE SSHA3-256 28 octets ATTRIBUTE SSHA3-384 29 octets ATTRIBUTE SSHA3-512 30 octets -# TOTP passwords and secrets -ATTRIBUTE TOTP 31 tlv - -BEGIN-TLV TOTP -ATTRIBUTE Secret 1 string secret -ATTRIBUTE Key 2 octets secret -ATTRIBUTE From-User 3 string -END-TLV TOTP - END-TLV Password diff --git a/share/dictionary/freeradius/dictionary.freeradius.internal.totp b/share/dictionary/freeradius/dictionary.freeradius.internal.totp new file mode 100644 index 0000000000000..2ecfd6a0696a1 --- /dev/null +++ b/share/dictionary/freeradius/dictionary.freeradius.internal.totp @@ -0,0 +1,21 @@ +# -*- text -*- +# Copyright (C) 2023 The FreeRADIUS Server project and contributors +# This work is licensed under CC-BY version 4.0 https://creativecommons.org/licenses/by/4.0 +# Version $Id$ +# +# Non Protocol Attributes used by FreeRADIUS +# +# $Id$ +# + +# +# TOTP token and secrets +# + +ATTRIBUTE TOTP 5300 tlv + +BEGIN-TLV TOTP +ATTRIBUTE Secret 1 string secret +ATTRIBUTE Key 2 octets secret +ATTRIBUTE From-User 3 string +END-TLV TOTP diff --git a/src/lib/totp/all.mk b/src/lib/totp/all.mk new file mode 100644 index 0000000000000..fdfe4a04d6733 --- /dev/null +++ b/src/lib/totp/all.mk @@ -0,0 +1,6 @@ +TARGETNAME := libfreeradius-totp +TARGET := $(TARGETNAME)$(L) + +SOURCES := totp.c + +src/freeradius-devel: | src/lib/totp/base.h diff --git a/src/lib/totp/base.h b/src/lib/totp/base.h new file mode 100644 index 0000000000000..7343bdb866771 --- /dev/null +++ b/src/lib/totp/base.h @@ -0,0 +1,44 @@ +#pragma once +/* + * This program 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 lib/totp/base.h + * @brief Common functions for TOTP library + * + * @copyright 2023 The FreeRADIUS server project + */ +RCSIDH(totp_h, "$Id$") + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + 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 +} fr_totp_t; + +int fr_totp_cmp(fr_totp_t const *cfg, time_t now, uint8_t const *key, size_t keylen, char const *totp); + +#ifdef __cplusplus +} +#endif diff --git a/src/lib/totp/totp.c b/src/lib/totp/totp.c new file mode 100644 index 0000000000000..375cc816bbc1e --- /dev/null +++ b/src/lib/totp/totp.c @@ -0,0 +1,135 @@ +/* + * 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 totp.c + * @brief Common function for TOTP validation. + * + * @copyright 2023 The FreeRADIUS server project + */ +#include +#include +#include + +/** Implement RFC 6238 TOTP algorithm (HMAC-SHA1). + * + * Appendix B has test vectors. Note that the test vectors are + * for 8-character challenges, and not for 6 character + * challenges! + * + * @param[in] cfg Instance of fr_totp_t + * @param[in] key Key to decrypt. + * @param[in] keylen Length of key field. + * @param[in] totp TOTP password entered by the user. + * @return + * - 0 On Success + * - -1 On Failure + */ +int fr_totp_cmp(fr_totp_t const *cfg, time_t now, uint8_t const *key, size_t keylen, char const *totp) +{ + time_t then; + unsigned int i; + uint8_t offset; + uint32_t challenge; + uint64_t padded; + uint8_t data[8]; + uint8_t digest[SHA1_DIGEST_LENGTH]; + char buffer[9]; + char buf_now[32], buf_then[32]; + + fr_assert(cfg != NULL); + fr_assert(cfg->otp_length == 6 || cfg->otp_length == 8); + fr_assert(key != NULL); + fr_assert(totp != NULL); + + if (!cfg) { + fr_strerror_const("Invalid 'cfg' parameter value."); + return -1; + } + + if (cfg->otp_length != 6 && cfg->otp_length != 8) { + fr_strerror_const("The 'cfg->opt_length' has incorrect length. Expected 6 or 8."); + return -1; + } + + if (keylen < 1) { + fr_strerror_const("Invalid 'keylen' parameter value."); + return -1; + } + + if (!totp || strlen(totp) < 1) { + fr_strerror_const("Invalid 'totp' parameter value."); + return -1; + } + + /* + * 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. + */ + + for (i = 0, then = now; i <= cfg->lookback_steps; i++, then -= cfg->lookback_steps) { + fr_sbuff_t snow = FR_SBUFF_IN(buf_now, sizeof(buf_now)); + fr_sbuff_t sthen = FR_SBUFF_IN(buf_then, sizeof(buf_then)); + + padded = ((uint64_t) now) / cfg->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), ((cfg->otp_length == 6) ? "%06u" : "%08u"), + challenge % ((cfg->otp_length == 6) ? 1000000 : 100000000)); + + fr_time_strftime_local(&snow, fr_time_wrap(now), "%a %b %d %H:%M:%S %Y"); + fr_time_strftime_local(&sthen, fr_time_wrap(then), "%a %b %d %H:%M:%S %Y"); + + DEBUG3("Now: %zu (%s), Then: %zu (%s)", (size_t) now, fr_sbuff_start(&snow), (size_t) then, fr_sbuff_start(&sthen)); + DEBUG3("Expected %s", buffer); + DEBUG3("Received %s", totp); + + if (fr_digest_cmp((uint8_t const *) buffer, (uint8_t const *) totp, cfg->otp_length) == 0) return 0; + } + + return -1; +} \ No newline at end of file diff --git a/src/modules/rlm_totp/Makefile b/src/modules/rlm_totp/Makefile deleted file mode 100644 index fbe24303db7b0..0000000000000 --- a/src/modules/rlm_totp/Makefile +++ /dev/null @@ -1,37 +0,0 @@ -# -# 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 -# -# ./totp totp