]> git.sur5r.net Git - openldap/commitdiff
Add TOTP pw mechanism
authorHoward Chu <hyc@openldap.org>
Thu, 2 Jul 2015 16:05:14 +0000 (17:05 +0100)
committerQuanah Gibson-Mount <quanah@openldap.org>
Tue, 18 Aug 2015 19:54:42 +0000 (14:54 -0500)
contrib/slapd-modules/passwd/totp/Makefile [new file with mode: 0644]
contrib/slapd-modules/passwd/totp/README [new file with mode: 0644]
contrib/slapd-modules/passwd/totp/slapd-totp.c [new file with mode: 0644]

diff --git a/contrib/slapd-modules/passwd/totp/Makefile b/contrib/slapd-modules/passwd/totp/Makefile
new file mode 100644 (file)
index 0000000..e25d316
--- /dev/null
@@ -0,0 +1,46 @@
+# $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_r/libldap_r.la \
+       $(LDAP_BUILD)/libraries/liblber/liblber.la
+
+LIBTOOL = $(LDAP_BUILD)/libtool
+CC = gcc
+OPT = -g -O2 -Wall
+DEFS = 
+INCS = $(LDAP_INC)
+LIBS = $(LDAP_LIB)
+
+PROGRAMS = pw-totp.la
+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)
+
+.SUFFIXES: .c .o .lo
+
+.c.lo:
+       $(LIBTOOL) --mode=compile $(CC) $(OPT) $(DEFS) $(INCS) -c $<
+
+all:           $(PROGRAMS)
+
+pw-totp.la:    slapd-totp.lo
+       $(LIBTOOL) --mode=link $(CC) $(OPT) -version-info $(LTVER) \
+       -rpath $(moduledir) -module -o $@ $? $(LIBS)
+
+clean:
+       rm -rf *.o *.lo *.la .libs
+
+install:       $(PROGRAMS)
+       mkdir -p $(DESTDIR)$(moduledir)
+       for p in $(PROGRAMS) ; do \
+               $(LIBTOOL) --mode=install cp $$p $(DESTDIR)$(moduledir) ; \
+       done
+
diff --git a/contrib/slapd-modules/passwd/totp/README b/contrib/slapd-modules/passwd/totp/README
new file mode 100644 (file)
index 0000000..df84f35
--- /dev/null
@@ -0,0 +1,75 @@
+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'.
+
+
+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-sha2.so
+
+5) This module also requires the use of the slapo-lastbind overlay. You
+should build that module and also add it to your slapd configuration.
+
+6) Restart slapd.
+
+
+Configuring
+-----------
+
+The {TOTP1}, {TOTP256}, and {TOTP512} 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}
+
+On the databases where your users reside you must configure both the
+lastbind overlay and the totp overlay:
+
+database mdb
+...
+
+overlay lastbind
+overlay totp
+
+
+
+Testing
+-------
+
+The TOTP1 algorithm is compatible with Google Authenticator.
+
+---
+
+This work is part of OpenLDAP Software <http://www.openldap.org/>.
+
+Copyright 2015 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 (file)
index 0000000..ec8fae6
--- /dev/null
@@ -0,0 +1,580 @@
+/* slapd-totp.c - Password module and overlay for TOTP */
+/* $OpenLDAP$ */
+/* This work is part of OpenLDAP Software <http://www.openldap.org/>.
+ *
+ * Copyright 2015 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>.
+ */
+
+#include <portable.h>
+
+#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>
+
+#include <openssl/sha.h>
+#include <openssl/hmac.h>
+
+#include "slap.h"
+#include "config.h"
+
+static LUTIL_PASSWD_CHK_FUNC chk_totp1, chk_totp256, chk_totp512;
+static LUTIL_PASSWD_HASH_FUNC hash_totp1, hash_totp256, hash_totp512;
+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 AttributeDescription *ad_authTimestamp;
+
+/* 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 = 1;
+
+               /* 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 */
+
+#define HMAC_setup(ctx, key, len, hash)        HMAC_CTX_init(&ctx); 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_cleanup(&ctx)
+
+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)
+{
+       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,
+       unsigned long tval,
+       int digits,
+       myval *out,
+       const void *mech)
+{
+       unsigned char digest[SHA512_DIGEST_LENGTH];
+       myval digval;
+       myval data;
+       unsigned char msg[8];
+       int i, offset, res, otp;
+
+#if !WORDS_BIGENDIAN
+       /* only needed on little-endian, can just use tval directly on big-endian */
+       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 );
+
+#define TIME_STEP      30
+#define DIGITS 6
+
+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 = time(0L) / TIME_STEP;
+       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 */
+       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) {
+                       long told = tt.tt_sec / TIME_STEP;
+                       if (told >= t)
+                               rc = LUTIL_PASSWD_ERR;
+               }
+       }       /* 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);
+       memset(key.mv_val, 0, key.mv_len);
+
+       /* compare */
+       if (out.mv_len != cred->bv_len)
+               return LUTIL_PASSWD_ERR;
+
+       rc = memcmp(out.mv_val, cred->bv_val, out.mv_len) ? LUTIL_PASSWD_ERR : LUTIL_PASSWD_OK;
+
+out:
+       ber_memfree(key.mv_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, EVP_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, EVP_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, EVP_sha512(), text);
+}
+
+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_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 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;
+       op->o_tmpfree( cb, op->o_tmpmemctx );
+       return 0;
+}
+
+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_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) {
+                       snprintf(cr->msg, sizeof(cr->msg), "unable to find authTimestamp attribute: %s (%d)",
+                               text, rc);
+                       Debug(LDAP_DEBUG_ANY, "totp: %s.\n", cr->msg, 0, 0);
+               }
+       }
+       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)
+               return rc;
+
+       return overlay_register(&totp);
+}
+
+int init_module(int argc, char *argv[]) {
+       return totp_initialize();
+}