diff options
Diffstat (limited to 'contrib/slapd-modules/passwd/totp')
-rw-r--r-- | contrib/slapd-modules/passwd/totp/Makefile | 58 | ||||
-rw-r--r-- | contrib/slapd-modules/passwd/totp/README | 87 | ||||
-rw-r--r-- | contrib/slapd-modules/passwd/totp/slapd-totp.c | 1000 | ||||
-rw-r--r-- | contrib/slapd-modules/passwd/totp/slapo-totp.5 | 109 |
4 files changed, 1254 insertions, 0 deletions
diff --git a/contrib/slapd-modules/passwd/totp/Makefile b/contrib/slapd-modules/passwd/totp/Makefile new file mode 100644 index 0000000..f7dff4b --- /dev/null +++ b/contrib/slapd-modules/passwd/totp/Makefile @@ -0,0 +1,58 @@ +# $OpenLDAP$ + +LDAP_SRC = ../../../.. +LDAP_BUILD = $(LDAP_SRC) +LDAP_INC = -I$(LDAP_BUILD)/include -I$(LDAP_SRC)/include -I$(LDAP_SRC)/servers/slapd +LDAP_LIB = $(LDAP_BUILD)/libraries/libldap/libldap.la \ + $(LDAP_BUILD)/libraries/liblber/liblber.la + +LIBTOOL = $(LDAP_BUILD)/libtool +INSTALL = /usr/bin/install +CC = gcc +OPT = -g -O2 +DEFS = +INCS = $(LDAP_INC) +LIBS = $(LDAP_LIB) + +PROGRAMS = pw-totp.la +MANPAGES = slapo-totp.5 +LTVER = 0:0:0 + +prefix=/usr/local +exec_prefix=$(prefix) +ldap_subdir=/openldap + +libdir=$(exec_prefix)/lib +libexecdir=$(exec_prefix)/libexec +moduledir = $(libexecdir)$(ldap_subdir) +mandir = $(exec_prefix)/share/man +man5dir = $(mandir)/man5 + +.SUFFIXES: .c .o .lo + +.c.lo: + $(LIBTOOL) --mode=compile $(CC) $(CFLAGS) $(OPT) $(CPPFLAGS) $(DEFS) $(INCS) -c $< + +all: $(PROGRAMS) + +pw-totp.la: slapd-totp.lo + $(LIBTOOL) --mode=link $(CC) $(LDFLAGS) -version-info $(LTVER) \ + -rpath $(moduledir) -module -o $@ $? $(LIBS) + +clean: + rm -rf *.o *.lo *.la .libs + +install: install-lib install-man FORCE + +install-lib: $(PROGRAMS) + mkdir -p $(DESTDIR)$(moduledir) + for p in $(PROGRAMS) ; do \ + $(LIBTOOL) --mode=install cp $$p $(DESTDIR)$(moduledir) ; \ + done + +install-man: $(MANPAGES) + mkdir -p $(DESTDIR)$(man5dir) + $(INSTALL) -m 644 $(MANPAGES) $(DESTDIR)$(man5dir) + +FORCE: + diff --git a/contrib/slapd-modules/passwd/totp/README b/contrib/slapd-modules/passwd/totp/README new file mode 100644 index 0000000..e6867f2 --- /dev/null +++ b/contrib/slapd-modules/passwd/totp/README @@ -0,0 +1,87 @@ +TOTP OpenLDAP support +---------------------- + +slapd-totp.c provides support for RFC 6238 TOTP Time-based One +Time Passwords in OpenLDAP using SHA-1, SHA-256, and SHA-512. +For instance, one could have the LDAP attribute: + +userPassword: {TOTP1}GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ + +which encodes the key '12345678901234567890'. + +It can also encode credentials consisting of a TOTP and a static +password. The format for this is: + +userPassword: {TOTP1ANDPW}GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ|<some_other_passwd> + +where <some_other_passwd> can be any scheme currently understood +by OpenLDAP. For example, using '{SHA}5en6G6MezRroT3XKqkdPOmY/BfQ=' +would encode the above TOTP with a static password of 'secret'. To +authenticate using this scheme, enter the static password immediately +followed by the TOTP, for example 'secret123456'. + + +Building +-------- + +1) Customize the LDAP_SRC variable in Makefile to point to the OpenLDAP +source root. + +2) Run 'make' to produce slapd-totp.so + +3) Copy slapd-totp.so somewhere permanent. + +4) Edit your slapd.conf (eg. /etc/ldap/slapd.conf), and add: + +moduleload ...path/to/slapd-totp.so + +5) This module replaces the function of the slapo-lastbind overlay. You +cannot use that overlay on the same database as this one. + +6) Restart slapd. + + +Configuring +----------- + +The {TOTP1}, {TOTP256}, {TOTP512}, {TOTP1ANDPW}, {TOTP256ANDPW}, +and {TOTP512ANDPW} password schemes should now be recognised. + +You can also tell OpenLDAP to use one of these new schemes when processing LDAP +Password Modify Extended Operations, thanks to the password-hash option in +slapd.conf. For example: + +password-hash {TOTP1} + +TOTP password schemes will only work on databases that have a rootdn and the +totp overlay configured: + +database mdb +rootdn "..." +... + +overlay totp + + + +Testing +------- + +The TOTP1 algorithm is compatible with Google Authenticator. + +--- + +This work is part of OpenLDAP Software <http://www.openldap.org/>. + +Copyright 2015-2022 The OpenLDAP Foundation. +Portions Copyright 2015 by Howard Chu, Symas Corp. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted only as authorized by the OpenLDAP +Public License. + +A copy of this license is available in the file LICENSE in the +top-level directory of the distribution or, alternatively, at +<http://www.OpenLDAP.org/license.html>. + diff --git a/contrib/slapd-modules/passwd/totp/slapd-totp.c b/contrib/slapd-modules/passwd/totp/slapd-totp.c new file mode 100644 index 0000000..25081e1 --- /dev/null +++ b/contrib/slapd-modules/passwd/totp/slapd-totp.c @@ -0,0 +1,1000 @@ +/* slapd-totp.c - Password module and overlay for TOTP */ +/* $OpenLDAP$ */ +/* This work is part of OpenLDAP Software <http://www.openldap.org/>. + * + * Copyright 2015-2022 The OpenLDAP Foundation. + * Portions Copyright 2015 by Howard Chu, Symas Corp. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted only as authorized by the OpenLDAP + * Public License. + * + * A copy of this license is available in the file LICENSE in the + * top-level directory of the distribution or, alternatively, at + * <http://www.OpenLDAP.org/license.html>. + */ +/* ACKNOWLEDGEMENTS: + * This work includes code from the lastbind overlay. + */ + +#include <portable.h> + +#if HAVE_STDINT_H +#include <stdint.h> +#endif + +#include <lber.h> +#include <lber_pvt.h> +#include "lutil.h" +#include <ac/stdlib.h> +#include <ac/ctype.h> +#include <ac/string.h> +/* include socket.h to get sys/types.h and/or winsock2.h */ +#include <ac/socket.h> + +#if HAVE_OPENSSL +#include <openssl/sha.h> +#include <openssl/hmac.h> + +#define TOTP_SHA512_DIGEST_LENGTH SHA512_DIGEST_LENGTH +#define TOTP_SHA1 EVP_sha1() +#define TOTP_SHA256 EVP_sha256() +#define TOTP_SHA512 EVP_sha512() +#define TOTP_HMAC_CTX HMAC_CTX * + +#if OPENSSL_VERSION_NUMBER < 0x10100000L +static HMAC_CTX *HMAC_CTX_new(void) +{ + HMAC_CTX *ctx = OPENSSL_malloc(sizeof(*ctx)); + if (ctx != NULL) { + HMAC_CTX_init(ctx); + } + return ctx; +} + +static void HMAC_CTX_free(HMAC_CTX *ctx) +{ + if (ctx != NULL) { + HMAC_CTX_cleanup(ctx); + OPENSSL_free(ctx); + } +} +#endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */ + +#define HMAC_setup(ctx, key, len, hash) \ + ctx = HMAC_CTX_new(); \ + HMAC_Init_ex(ctx, key, len, hash, 0) +#define HMAC_crunch(ctx, buf, len) HMAC_Update(ctx, buf, len) +#define HMAC_finish(ctx, dig, dlen) \ + HMAC_Final(ctx, dig, &dlen); \ + HMAC_CTX_free(ctx) + +#elif HAVE_GNUTLS +#include <nettle/hmac.h> + +#define TOTP_SHA512_DIGEST_LENGTH SHA512_DIGEST_SIZE +#define TOTP_SHA1 &nettle_sha1 +#define TOTP_SHA256 &nettle_sha256 +#define TOTP_SHA512 &nettle_sha512 +#define TOTP_HMAC_CTX struct hmac_sha512_ctx + +#define HMAC_setup(ctx, key, len, hash) \ + const struct nettle_hash *h=hash;\ + hmac_set_key(&ctx.outer, &ctx.inner, &ctx.state, h, len, key) +#define HMAC_crunch(ctx, buf, len) hmac_update(&ctx.state, h, len, buf) +#define HMAC_finish(ctx, dig, dlen) \ + hmac_digest(&ctx.outer, &ctx.inner, &ctx.state, h, h->digest_size, dig);\ + dlen = h->digest_size + +#else +# error Unsupported crypto backend. +#endif + +#include "slap.h" +#include "slap-config.h" + +static LUTIL_PASSWD_CHK_FUNC chk_totp1, chk_totp256, chk_totp512, + chk_totp1andpw, chk_totp256andpw, chk_totp512andpw; +static LUTIL_PASSWD_HASH_FUNC hash_totp1, hash_totp256, hash_totp512, + hash_totp1andpw, hash_totp256andpw, hash_totp512andpw; +static const struct berval scheme_totp1 = BER_BVC("{TOTP1}"); +static const struct berval scheme_totp256 = BER_BVC("{TOTP256}"); +static const struct berval scheme_totp512 = BER_BVC("{TOTP512}"); +static const struct berval scheme_totp1andpw = BER_BVC("{TOTP1ANDPW}"); +static const struct berval scheme_totp256andpw = BER_BVC("{TOTP256ANDPW}"); +static const struct berval scheme_totp512andpw = BER_BVC("{TOTP512ANDPW}"); + +static AttributeDescription *ad_authTimestamp; + +/* This is the definition used by ISODE, as supplied to us in + * ITS#6238 Followup #9 + */ +static struct schema_info { + char *def; + AttributeDescription **ad; +} totp_OpSchema[] = { + { "( 1.3.6.1.4.1.453.16.2.188 " + "NAME 'authTimestamp' " + "DESC 'last successful authentication using any method/mech' " + "EQUALITY generalizedTimeMatch " + "ORDERING generalizedTimeOrderingMatch " + "SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 " + "SINGLE-VALUE NO-USER-MODIFICATION USAGE dsaOperation )", + &ad_authTimestamp}, + { NULL, NULL } +}; + +/* RFC3548 base32 encoding/decoding */ + +static const char Base32[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +static const char Pad32 = '='; + +static int +totp_b32_ntop( + u_char const *src, + size_t srclength, + char *target, + size_t targsize) +{ + size_t datalength = 0; + u_char input0; + u_int input1; /* assumed to be at least 32 bits */ + u_char output[8]; + int i; + + while (4 < srclength) { + if (datalength + 8 > targsize) + return (-1); + input0 = *src++; + input1 = *src++; + input1 <<= 8; + input1 |= *src++; + input1 <<= 8; + input1 |= *src++; + input1 <<= 8; + input1 |= *src++; + srclength -= 5; + + for (i=7; i>1; i--) { + output[i] = input1 & 0x1f; + input1 >>= 5; + } + output[0] = input0 >> 3; + output[1] = (input0 & 0x07) << 2 | input1; + + for (i=0; i<8; i++) + target[datalength++] = Base32[output[i]]; + } + + /* Now we worry about padding. */ + if (0 != srclength) { + static const int outlen[] = { 2,4,5,7 }; + int n; + if (datalength + 8 > targsize) + return (-1); + + /* Get what's left. */ + input1 = *src++; + for (i = 1; i < srclength; i++) { + input1 <<= 8; + input1 |= *src++; + } + input1 <<= 8 * (4-srclength); + n = outlen[srclength-1]; + for (i=0; i<n; i++) { + target[datalength++] = Base32[(input1 & 0xf8000000) >> 27]; + input1 <<= 5; + } + for (; i<8; i++) + target[datalength++] = Pad32; + } + if (datalength >= targsize) + return (-1); + target[datalength] = '\0'; /* Returned value doesn't count \0. */ + return (datalength); +} + +/* converts characters, eight at a time, starting at src + from base - 32 numbers into five 8 bit bytes in the target area. + it returns the number of data bytes stored at the target, or -1 on error. + */ + +static int +totp_b32_pton( + char const *src, + u_char *target, + size_t targsize) +{ + int tarindex, state, ch; + char *pos; + + state = 0; + tarindex = 0; + + while ((ch = *src++) != '\0') { + if (ch == Pad32) + break; + + pos = strchr(Base32, ch); + if (pos == 0) /* A non-base32 character. */ + return (-1); + + switch (state) { + case 0: + if (target) { + if ((size_t)tarindex >= targsize) + return (-1); + target[tarindex] = (pos - Base32) << 3; + } + state = 1; + break; + case 1: + if (target) { + if ((size_t)tarindex + 1 >= targsize) + return (-1); + target[tarindex] |= (pos - Base32) >> 2; + target[tarindex+1] = ((pos - Base32) & 0x3) + << 6 ; + } + tarindex++; + state = 2; + break; + case 2: + if (target) { + target[tarindex] |= (pos - Base32) << 1; + } + state = 3; + break; + case 3: + if (target) { + if ((size_t)tarindex + 1 >= targsize) + return (-1); + target[tarindex] |= (pos - Base32) >> 4; + target[tarindex+1] = ((pos - Base32) & 0xf) + << 4 ; + } + tarindex++; + state = 4; + break; + case 4: + if (target) { + if ((size_t)tarindex + 1 >= targsize) + return (-1); + target[tarindex] |= (pos - Base32) >> 1; + target[tarindex+1] = ((pos - Base32) & 0x1) + << 7 ; + } + tarindex++; + state = 5; + break; + case 5: + if (target) { + target[tarindex] |= (pos - Base32) << 2; + } + state = 6; + break; + case 6: + if (target) { + if ((size_t)tarindex + 1 >= targsize) + return (-1); + target[tarindex] |= (pos - Base32) >> 3; + target[tarindex+1] = ((pos - Base32) & 0x7) + << 5 ; + } + tarindex++; + state = 7; + break; + case 7: + if (target) { + target[tarindex] |= (pos - Base32); + } + state = 0; + tarindex++; + break; + + default: + abort(); + } + } + + /* + * We are done decoding Base-32 chars. Let's see if we ended + * on a byte boundary, and/or with erroneous trailing characters. + */ + + if (ch == Pad32) { /* We got a pad char. */ + int i = 0; + + /* count pad chars */ + for (; ch; ch = *src++) { + if (ch != Pad32) + return (-1); + i++; + } + /* there are only 4 valid ending states with a + * pad character, make sure the number of pads is valid. + */ + switch(state) { + case 2: if (i != 6) return -1; + break; + case 4: if (i != 4) return -1; + break; + case 5: if (i != 3) return -1; + break; + case 7: if (i != 1) return -1; + break; + default: + return -1; + } + /* + * Now make sure that the "extra" bits that slopped past + * the last full byte were zeros. If we don't check them, + * they become a subliminal channel. + */ + if (target && target[tarindex] != 0) + return (-1); + } else { + /* + * We ended by seeing the end of the string. Make sure we + * have no partial bytes lying around. + */ + if (state != 0) + return (-1); + } + + return (tarindex); +} + +/* RFC6238 TOTP */ + + +typedef struct myval { + ber_len_t mv_len; + void *mv_val; +} myval; + +static void do_hmac( + const void *hash, + myval *key, + myval *data, + myval *out) +{ + TOTP_HMAC_CTX ctx; + unsigned int digestLen; + + HMAC_setup(ctx, key->mv_val, key->mv_len, hash); + HMAC_crunch(ctx, data->mv_val, data->mv_len); + HMAC_finish(ctx, out->mv_val, digestLen); + out->mv_len = digestLen; +} + +static const int DIGITS_POWER[] = { + 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 }; + +static void generate( + myval *key, + uint64_t tval, + int digits, + myval *out, + const void *mech) +{ + unsigned char digest[TOTP_SHA512_DIGEST_LENGTH]; + myval digval; + myval data; + unsigned char msg[8]; + int i, offset, res, otp; + +#if WORDS_BIGENDIAN + *(uint64_t *)msg = tval; +#else + for (i=7; i>=0; i--) { + msg[i] = tval & 0xff; + tval >>= 8; + } +#endif + + data.mv_val = msg; + data.mv_len = sizeof(msg); + + digval.mv_val = digest; + digval.mv_len = sizeof(digest); + do_hmac(mech, key, &data, &digval); + + offset = digest[digval.mv_len-1] & 0xf; + res = ((digest[offset] & 0x7f) << 24) | + ((digest[offset+1] & 0xff) << 16) | + ((digest[offset+2] & 0xff) << 8) | + (digest[offset+3] & 0xff); + + otp = res % DIGITS_POWER[digits]; + out->mv_len = snprintf(out->mv_val, out->mv_len, "%0*d", digits, otp); +} + +static int totp_op_cleanup( Operation *op, SlapReply *rs ); +static int totp_bind_response( Operation *op, SlapReply *rs ); + +#define TIME_STEP 30 +#define DIGITS 6 +#define DELIM '|' /* a single character */ +#define TOTP_AND_PW_HASH_SCHEME "{SSHA}" + +static int chk_totp( + const struct berval *passwd, + const struct berval *cred, + const void *mech, + const char **text) +{ + void *ctx, *op_tmp; + Operation *op; + Entry *e; + Attribute *a; + long t, told = 0; + int rc; + myval out, key; + char outbuf[32]; + + /* Find our thread context, find our Operation */ + ctx = ldap_pvt_thread_pool_context(); + if (ldap_pvt_thread_pool_getkey(ctx, totp_op_cleanup, &op_tmp, NULL) || + !op_tmp) + return LUTIL_PASSWD_ERR; + op = op_tmp; + + rc = be_entry_get_rw(op, &op->o_req_ndn, NULL, NULL, 0, &e); + if (rc != LDAP_SUCCESS) return LUTIL_PASSWD_ERR; + + /* Make sure previous login is older than current time */ + t = op->o_time / TIME_STEP; + a = attr_find(e->e_attrs, ad_authTimestamp); + if (a) { + struct lutil_tm tm; + struct lutil_timet tt; + if (lutil_parsetime(a->a_vals[0].bv_val, &tm) == 0 && + lutil_tm2time(&tm, &tt) == 0) { + told = tt.tt_sec / TIME_STEP; + if (told >= t) + rc = LUTIL_PASSWD_ERR; + } + if (!rc) { /* seems OK, remember old stamp */ + slap_callback *sc; + for (sc = op->o_callback; sc; sc = sc->sc_next) { + if (sc->sc_response == totp_bind_response) { + sc->sc_private = ber_dupbv_x(NULL, &a->a_vals[0], op->o_tmpmemctx); + break; + } + } + } + } /* else no previous login, 1st use is OK */ + + be_entry_release_r(op, e); + if (rc) return rc; + + /* Key is stored in base32 */ + key.mv_len = passwd->bv_len * 5 / 8; + key.mv_val = ber_memalloc(key.mv_len+1); + + if (!key.mv_val) + return LUTIL_PASSWD_ERR; + + rc = totp_b32_pton(passwd->bv_val, key.mv_val, key.mv_len); + if (rc < 1) { + rc = LUTIL_PASSWD_ERR; + goto out; + } + + out.mv_val = outbuf; + out.mv_len = sizeof(outbuf); + generate(&key, t, DIGITS, &out, mech); + + /* compare */ + if (out.mv_len != cred->bv_len) { + rc = LUTIL_PASSWD_ERR; + goto out; + } + + rc = memcmp(out.mv_val, cred->bv_val, out.mv_len) ? LUTIL_PASSWD_ERR : LUTIL_PASSWD_OK; + + /* If current value doesn't match, try again with previous value + * but only if the most recent login is older than the previous + * time step but still set */ + if (rc == LUTIL_PASSWD_ERR && told < t - 1 && told > 0) { + out.mv_val = outbuf; + out.mv_len = sizeof(outbuf); + generate(&key, t - 1, DIGITS, &out, mech); + /* compare */ + if (out.mv_len != cred->bv_len) + goto out; + rc = memcmp(out.mv_val, cred->bv_val, out.mv_len) ? LUTIL_PASSWD_ERR : LUTIL_PASSWD_OK; + } + +out: + memset(key.mv_val, 0, key.mv_len); + ber_memfree(key.mv_val); + return rc; +} + +static int chk_totp_and_pw( + const struct berval *scheme, + const struct berval *passwd, + const struct berval *cred, + const char **text, + const void *mech) +{ + char *s; + int rc = LUTIL_PASSWD_ERR, rc_pass, rc_otp; + ber_len_t len; + struct berval cred_pass, cred_otp, passwd_pass, passwd_otp; + + /* Check credential length, no point to continue if too short */ + if (cred->bv_len <= DIGITS) + return rc; + + /* The OTP seed of the stored password */ + s = strchr(passwd->bv_val, DELIM); + if (s) { + len = s - passwd->bv_val; + } else { + return rc; + } + if (!ber_str2bv(passwd->bv_val, len, 1, &passwd_otp)) + return rc; + + /* The password part of the stored password */ + s++; + ber_str2bv(s, 0, 0, &passwd_pass); + + /* The OTP part of the entered credential */ + ber_str2bv(&cred->bv_val[cred->bv_len - DIGITS], DIGITS, 0, &cred_otp); + + /* The password part of the entered credential */ + if (!ber_str2bv(cred->bv_val, cred->bv_len - DIGITS, 0, &cred_pass)) { + /* Cleanup */ + memset(passwd_otp.bv_val, 0, passwd_otp.bv_len); + ber_memfree(passwd_otp.bv_val); + return rc; + } + + rc_otp = chk_totp(&passwd_otp, &cred_otp, mech, text); + rc_pass = lutil_passwd(&passwd_pass, &cred_pass, NULL, text); + if (rc_otp == LUTIL_PASSWD_OK && rc_pass == LUTIL_PASSWD_OK) + rc = LUTIL_PASSWD_OK; + + /* Cleanup and return */ + memset(passwd_otp.bv_val, 0, passwd_otp.bv_len); + ber_memfree(passwd_otp.bv_val); + + return rc; +} + +static int chk_totp1( + const struct berval *scheme, + const struct berval *passwd, + const struct berval *cred, + const char **text) +{ + return chk_totp(passwd, cred, TOTP_SHA1, text); +} + +static int chk_totp256( + const struct berval *scheme, + const struct berval *passwd, + const struct berval *cred, + const char **text) +{ + return chk_totp(passwd, cred, TOTP_SHA256, text); +} + +static int chk_totp512( + const struct berval *scheme, + const struct berval *passwd, + const struct berval *cred, + const char **text) +{ + return chk_totp(passwd, cred, TOTP_SHA512, text); +} + +static int chk_totp1andpw( + const struct berval *scheme, + const struct berval *passwd, + const struct berval *cred, + const char **text) +{ + return chk_totp_and_pw(scheme, passwd, cred, text, TOTP_SHA1); +} + +static int chk_totp256andpw( + const struct berval *scheme, + const struct berval *passwd, + const struct berval *cred, + const char **text) +{ + return chk_totp_and_pw(scheme, passwd, cred, text, TOTP_SHA256); +} + +static int chk_totp512andpw( + const struct berval *scheme, + const struct berval *passwd, + const struct berval *cred, + const char **text) +{ + return chk_totp_and_pw(scheme, passwd, cred, text, TOTP_SHA512); +} + +static int passwd_string32( + const struct berval *scheme, + const struct berval *passwd, + struct berval *hash) +{ + int b32len = (passwd->bv_len + 4)/5 * 8; + int rc; + hash->bv_len = scheme->bv_len + b32len; + hash->bv_val = ber_memalloc(hash->bv_len + 1); + AC_MEMCPY(hash->bv_val, scheme->bv_val, scheme->bv_len); + rc = totp_b32_ntop((unsigned char *)passwd->bv_val, passwd->bv_len, + hash->bv_val + scheme->bv_len, b32len+1); + if (rc < 0) { + ber_memfree(hash->bv_val); + hash->bv_val = NULL; + return LUTIL_PASSWD_ERR; + } + return LUTIL_PASSWD_OK; +} + +static int hash_totp_and_pw( + const struct berval *scheme, + const struct berval *passwd, + struct berval *hash, + const char **text) +{ + struct berval otp, pass, hash_otp, hash_pass; + ber_len_t len; + char *s; + int rc = LUTIL_PASSWD_ERR; + + /* The OTP seed part */ + s = strchr(passwd->bv_val, DELIM); + if (s) { + len = s - passwd->bv_val; + } else { + return rc; + } + if (!ber_str2bv(passwd->bv_val, len, 0, &otp)) + return rc; + + /* The static password part */ + s++; + ber_str2bv(s, 0, 0, &pass); + + /* Hash the OTP seed */ + rc = passwd_string32(scheme, &otp, &hash_otp); + + /* If successful, hash the static password, else cleanup and return */ + if (rc == LUTIL_PASSWD_OK) { + rc = lutil_passwd_hash(&pass, TOTP_AND_PW_HASH_SCHEME, + &hash_pass, text); + } else { + return LUTIL_PASSWD_ERR; + } + + /* If successful, allocate memory to combine them, else cleanup + * and return */ + if (rc == LUTIL_PASSWD_OK) { + /* Add 1 character to bv_len to hold DELIM */ + hash->bv_len = hash_pass.bv_len + hash_otp.bv_len + 1; + hash->bv_val = ber_memalloc(hash->bv_len + 1); + if (!hash->bv_val) + rc = LUTIL_PASSWD_ERR; + } else { + memset(hash_otp.bv_val, 0, hash_otp.bv_len); + ber_memfree(hash_otp.bv_val); + return LUTIL_PASSWD_ERR; + } + + /* If successful, combine the two hashes with the delimiter */ + if (rc == LUTIL_PASSWD_OK) { + AC_MEMCPY(hash->bv_val, hash_otp.bv_val, hash_otp.bv_len); + hash->bv_val[hash_otp.bv_len] = DELIM; + AC_MEMCPY(hash->bv_val + hash_otp.bv_len + 1, + hash_pass.bv_val, hash_pass.bv_len); + hash->bv_val[hash->bv_len] = '\0'; + } + + /* Cleanup and return */ + memset(hash_otp.bv_val, 0, hash_otp.bv_len); + memset(hash_pass.bv_val, 0, hash_pass.bv_len); + ber_memfree(hash_otp.bv_val); + ber_memfree(hash_pass.bv_val); + + return rc; +} + +static int hash_totp1( + const struct berval *scheme, + const struct berval *passwd, + struct berval *hash, + const char **text) +{ +#if 0 + if (passwd->bv_len != SHA_DIGEST_LENGTH) { + *text = "invalid key length"; + return LUTIL_PASSWD_ERR; + } +#endif + return passwd_string32(scheme, passwd, hash); +} + +static int hash_totp256( + const struct berval *scheme, + const struct berval *passwd, + struct berval *hash, + const char **text) +{ +#if 0 + if (passwd->bv_len != SHA256_DIGEST_LENGTH) { + *text = "invalid key length"; + return LUTIL_PASSWD_ERR; + } +#endif + return passwd_string32(scheme, passwd, hash); +} + +static int hash_totp512( + const struct berval *scheme, + const struct berval *passwd, + struct berval *hash, + const char **text) +{ +#if 0 + if (passwd->bv_len != SHA512_DIGEST_LENGTH) { + *text = "invalid key length"; + return LUTIL_PASSWD_ERR; + } +#endif + return passwd_string32(scheme, passwd, hash); +} + +static int hash_totp1andpw( + const struct berval *scheme, + const struct berval *passwd, + struct berval *hash, + const char **text) +{ +#if 0 + if (passwd->bv_len != SHA_DIGEST_LENGTH) { + *text = "invalid key length"; + return LUTIL_PASSWD_ERR; + } +#endif + return hash_totp_and_pw(scheme, passwd, hash, text); +} + +static int hash_totp256andpw( + const struct berval *scheme, + const struct berval *passwd, + struct berval *hash, + const char **text) +{ +#if 0 + if (passwd->bv_len != SHA256_DIGEST_LENGTH) { + *text = "invalid key length"; + return LUTIL_PASSWD_ERR; + } +#endif + return hash_totp_and_pw(scheme, passwd, hash, text); +} + +static int hash_totp512andpw( + const struct berval *scheme, + const struct berval *passwd, + struct berval *hash, + const char **text) +{ +#if 0 + if (passwd->bv_len != SHA512_DIGEST_LENGTH) { + *text = "invalid key length"; + return LUTIL_PASSWD_ERR; + } +#endif + return hash_totp_and_pw(scheme, passwd, hash, text); +} + +static int totp_op_cleanup( + Operation *op, + SlapReply *rs ) +{ + slap_callback *cb; + + /* clear out the current key */ + ldap_pvt_thread_pool_setkey( op->o_threadctx, totp_op_cleanup, + NULL, 0, NULL, NULL ); + + /* free the callback */ + cb = op->o_callback; + op->o_callback = cb->sc_next; + if (cb->sc_private) + ber_bvfree_x(cb->sc_private, op->o_tmpmemctx); + op->o_tmpfree( cb, op->o_tmpmemctx ); + return 0; +} + +static int +totp_bind_response( Operation *op, SlapReply *rs ) +{ + Modifications *mod = NULL; + BackendInfo *bi = op->o_bd->bd_info; + Entry *e; + int rc; + + /* we're only interested if the bind was successful */ + if ( rs->sr_err != LDAP_SUCCESS ) + return SLAP_CB_CONTINUE; + + rc = be_entry_get_rw( op, &op->o_req_ndn, NULL, NULL, 0, &e ); + op->o_bd->bd_info = bi; + + if ( rc != LDAP_SUCCESS ) { + return SLAP_CB_CONTINUE; + } + + { + time_t now; + Attribute *a; + Modifications *m; + char nowstr[ LDAP_LUTIL_GENTIME_BUFSIZE ]; + struct berval timestamp; + + /* get the current time */ + now = op->o_time; + + /* update the authTimestamp in the user's entry with the current time */ + timestamp.bv_val = nowstr; + timestamp.bv_len = sizeof(nowstr); + slap_timestamp( &now, ×tamp ); + + m = ch_calloc( sizeof(Modifications), 1 ); + m->sml_op = LDAP_MOD_REPLACE; + m->sml_flags = 0; + m->sml_type = ad_authTimestamp->ad_cname; + m->sml_desc = ad_authTimestamp; + m->sml_numvals = 1; + m->sml_values = ch_calloc( sizeof(struct berval), 2 ); + m->sml_nvalues = ch_calloc( sizeof(struct berval), 2 ); + + ber_dupbv( &m->sml_values[0], ×tamp ); + ber_dupbv( &m->sml_nvalues[0], ×tamp ); + m->sml_next = mod; + mod = m; + + /* get authTimestamp attribute, if it exists */ + if ((a = attr_find( e->e_attrs, ad_authTimestamp)) != NULL && op->o_callback->sc_private) { + struct berval *bv = op->o_callback->sc_private; + m = ch_calloc( sizeof(Modifications), 1 ); + m->sml_op = LDAP_MOD_DELETE; + m->sml_flags = 0; + m->sml_type = ad_authTimestamp->ad_cname; + m->sml_desc = ad_authTimestamp; + m->sml_numvals = 1; + m->sml_values = ch_calloc( sizeof(struct berval), 2 ); + m->sml_nvalues = ch_calloc( sizeof(struct berval), 2 ); + + ber_dupbv( &m->sml_values[0], bv ); + ber_dupbv( &m->sml_nvalues[0], bv ); + m->sml_next = mod; + mod = m; + } + } + + be_entry_release_r( op, e ); + + /* perform the update */ + if ( mod ) { + Operation op2 = *op; + SlapReply r2 = { REP_RESULT }; + slap_callback cb = { NULL, slap_null_cb, NULL, NULL }; + + /* This is a DSA-specific opattr, it never gets replicated. */ + op2.o_tag = LDAP_REQ_MODIFY; + op2.o_callback = &cb; + op2.orm_modlist = mod; + op2.o_dn = op->o_bd->be_rootdn; + op2.o_ndn = op->o_bd->be_rootndn; + op2.o_dont_replicate = 1; + rc = op->o_bd->be_modify( &op2, &r2 ); + slap_mods_free( mod, 1 ); + if (rc != LDAP_SUCCESS) { + /* slapd has logged this as a success already, but we + * need to fail it because the authTimestamp changed + * out from under us. + */ + rs->sr_err = LDAP_INVALID_CREDENTIALS; + connection2anonymous(op->o_conn); + op2 = *op; + op2.o_callback = NULL; + send_ldap_result(&op2, rs); + op->o_bd->bd_info = bi; + return rs->sr_err; + } + } + + op->o_bd->bd_info = bi; + return SLAP_CB_CONTINUE; +} + +static int totp_op_bind( + Operation *op, + SlapReply *rs ) +{ + /* If this is a simple Bind, stash the Op pointer so our chk + * function can find it. Set a cleanup callback to clear it + * out when the Bind completes. + */ + if ( op->oq_bind.rb_method == LDAP_AUTH_SIMPLE ) { + slap_callback *cb; + ldap_pvt_thread_pool_setkey( op->o_threadctx, + totp_op_cleanup, op, 0, NULL, NULL ); + cb = op->o_tmpcalloc( 1, sizeof(slap_callback), op->o_tmpmemctx ); + cb->sc_response = totp_bind_response; + cb->sc_cleanup = totp_op_cleanup; + cb->sc_next = op->o_callback; + op->o_callback = cb; + } + return SLAP_CB_CONTINUE; +} + +static int totp_db_open( + BackendDB *be, + ConfigReply *cr +) +{ + int rc = 0; + + if (!ad_authTimestamp) { + const char *text = NULL; + rc = slap_str2ad("authTimestamp", &ad_authTimestamp, &text); + if (rc) { + rc = register_at(totp_OpSchema[0].def, totp_OpSchema[0].ad, 0 ); + if (rc) { + snprintf(cr->msg, sizeof(cr->msg), "unable to find or register authTimestamp attribute: %s (%d)", + text, rc); + Debug(LDAP_DEBUG_ANY, "totp: %s.\n", cr->msg ); + } + ad_authTimestamp->ad_type->sat_flags |= SLAP_AT_MANAGEABLE; + } + } + return rc; +} + +static slap_overinst totp; + +int +totp_initialize(void) +{ + int rc; + + totp.on_bi.bi_type = "totp"; + + totp.on_bi.bi_db_open = totp_db_open; + totp.on_bi.bi_op_bind = totp_op_bind; + + rc = lutil_passwd_add((struct berval *) &scheme_totp1, chk_totp1, hash_totp1); + if (!rc) + rc = lutil_passwd_add((struct berval *) &scheme_totp256, chk_totp256, hash_totp256); + if (!rc) + rc = lutil_passwd_add((struct berval *) &scheme_totp512, chk_totp512, hash_totp512); + if (!rc) + rc = lutil_passwd_add((struct berval *) &scheme_totp1andpw, chk_totp1andpw, hash_totp1andpw); + if (!rc) + rc = lutil_passwd_add((struct berval *) &scheme_totp256andpw, chk_totp256andpw, hash_totp256andpw); + if (!rc) + rc = lutil_passwd_add((struct berval *) &scheme_totp512andpw, chk_totp512andpw, hash_totp512andpw); + if (rc) + return rc; + + return overlay_register(&totp); +} + +int init_module(int argc, char *argv[]) { + return totp_initialize(); +} diff --git a/contrib/slapd-modules/passwd/totp/slapo-totp.5 b/contrib/slapd-modules/passwd/totp/slapo-totp.5 new file mode 100644 index 0000000..7c99bf1 --- /dev/null +++ b/contrib/slapd-modules/passwd/totp/slapo-totp.5 @@ -0,0 +1,109 @@ +.TH PW-TOTP 5 "2015/7/2" "PW-TOTP" +.\" Copyright 2015-2022 The OpenLDAP Foundation. +.\" Portions Copyright 2015 by Howard Chu, Symas Corp. All rights reserved. +.\" Copying restrictions apply. See COPYRIGHT/LICENSE. +.SH NAME +pw-totp \- TOTP Password handling module +.SH SYNOPSIS +.B moduleload +.I pw-totp.la +.SH DESCRIPTION +The +.B pw-totp +module allows time-based one-time password, AKA "authenticator-style", +authentication to be added to applications that use LDAP for +authentication. In most cases no changes to the applications are needed to switch +to this type of authentication. + +With this module, the password needed for a user to authenticate is calculated +based on the current time and a key that is stored in the user's LDAP entry. Since +the password is based on the time, it changes periodically. Once used, it cannot be +used again so keyloggers and shoulder-surfers are thwarted. A mobile +phone application, such as the Google Authenticator (a 'prover'), can be used +to calculate the user's current password, which is expressed as a six-digit +number. +Alternatively, the value can be calculated by some other application with access +to the user's key and delivered to the user through SMS or some other channel. +When prompted to authenticate, the user merely enters the six-digit code provided by +the prover. + +Additionally, the overlay can also authenticate TOTP passwords +combined with a static password. To do this, utilize one of the +{TOTP1ANDPW}, {TOTP256ANDPW}, or {TOTP512ANDPW} password schemes +and append the static password scheme value to the end of the +userPassword attribute, separated by a pipe (|) character. + +This implementation complies with +.B RFC 6238 TOTP Time-based One Time Passwords +and includes support for the SHA-1, SHA-256, and SHA-512 HMAC +algorithms. + +The HMAC key used in the TOTP computation is stored in the userPassword attribute +of the user's LDAP entry and the LDAP Password Modify Extended Operation is used to +set and change the value. The +value should correspond to that used by the the prover (authenticator). + +.SH CONFIGURATION +Once the module is loaded with the moduleload command from the synopsis, +the {TOTP1}, {TOTP256}, {TOTP512} +{TOTP1ANDPW}, {TOTP256ANDPW}, and {TOTP512ANDPW} +password schemes will be recognized. + +On the databases where your users reside you must configure the +totp overlay: + +.nf + database mdb + \... + overlay totp + \... +.fi + +You can tell OpenLDAP to use one of these new schemes when processing LDAP +Password Modify Extended Operations, thanks to the password-hash option in +slapd.conf. For example: + +.nf + password-hash {TOTP256} +.fi + +.SH NOTES +This module includes functionality implemented by the slapo-lastbind overlay +and cannot coexist with it in the same database. Also note +that since the time that the last bind occurred +is needed to properly implement TOTP, provisions need to be made to propagate +the authTimestamp attribute to other servers that are providing authentication +services. + +The hash functions for the {TOTP1ANDPW}, {TOTP256ANDPW}, and {TOTP512ANDPW} +schemes expect the secret to be entered in the form: +<OTP seed><DELIM><static password>, where DELIM is currently defined +as the pipe character (|). + +.SH BUGS +The time step is hard-coded to thirty seconds. This should be OK for many use cases, +but it would be nice if the value +could be changed with a configuration keyword or in an attribute value. +However, after one successful initial authentication (to verify +the clocks on the server and the user's prover are in sync) the TOTP +value of the previous time window may also be used to successfully +authenticate, provided no successful bind has been performed already +in the current or previous time window. This eliminates false +negatives caused by user or network delays +entering or transmitting the TOTP value. + +The authenticator code that is generated is hard-coded to a length of six digits. +While in most cases +this is probably better than the alternative length of four digits, there may be +cases where a four-digit value is preferred. + +In cases where password-hash lists multiple mechanisms, the TOTP key will also +be changed at the same time. This is likely to be undesirable behavior. + +.SH "SEE ALSO" +.BR slapd.conf (5) ldappasswd (1) +.SH ACKNOWLEDGEMENT +This work was developed by Howard Chu of Symas Corporation for inclusion in +OpenLDAP Software. + +Password + TOTP support added by Greg Veldman on behalf of SCinet. |