krb5 commit: Add server-side otp preauth plugin

Greg Hudson ghudson at MIT.EDU
Thu Jul 11 14:24:39 EDT 2013


https://github.com/krb5/krb5/commit/4b5dd8bcfb10af254fb9efbe4cf39befe5b1e6ac
commit 4b5dd8bcfb10af254fb9efbe4cf39befe5b1e6ac
Author: Nathaniel McCallum <npmccallum at redhat.com>
Date:   Wed Apr 3 12:38:05 2013 -0400

    Add server-side otp preauth plugin
    
    This plugin implements the proposal for providing OTP support by
    proxying requests to RADIUS. Details can be found inside the
    provided documentation as well as on the project page.
    
    http://k5wiki.kerberos.org/wiki/Projects/OTPOverRADIUS
    
    ticket: 7678

 doc/admin/conf_files/kdc_conf.rst   |   66 ++++
 doc/admin/index.rst                 |    1 +
 doc/admin/otp.rst                   |   85 +++++
 src/Makefile.in                     |    1 +
 src/configure.in                    |    1 +
 src/kdc/kdc_preauth.c               |    2 +
 src/plugins/preauth/otp/Makefile.in |   31 ++
 src/plugins/preauth/otp/deps        |   26 ++
 src/plugins/preauth/otp/main.c      |  379 ++++++++++++++++++++
 src/plugins/preauth/otp/otp.exports |    1 +
 src/plugins/preauth/otp/otp_state.c |  649 +++++++++++++++++++++++++++++++++++
 src/plugins/preauth/otp/otp_state.h |   59 ++++
 src/tests/Makefile.in               |    1 +
 src/tests/t_otp.py                  |  226 ++++++++++++
 14 files changed, 1528 insertions(+), 0 deletions(-)

diff --git a/doc/admin/conf_files/kdc_conf.rst b/doc/admin/conf_files/kdc_conf.rst
index c7007d6..3b56e61 100644
--- a/doc/admin/conf_files/kdc_conf.rst
+++ b/doc/admin/conf_files/kdc_conf.rst
@@ -491,6 +491,72 @@ administrative server will be appended to the file
         admin_server = DEVICE=/dev/tty04
 
 
+.. _otp:
+
+[otp]
+~~~~~
+
+Each subsection of [otp] is the name of an OTP token type.  The tags
+within the subsection define the configuration required to forward a
+One Time Password request to a RADIUS server.
+
+For each token type, the following tags may be specified:
+
+**server**
+    This is the server to send the RADIUS request to.  It can be a
+    hostname with optional port, an ip address with optional port, or
+    a Unix domain socket address.  The default is
+    |kdcdir|\ ``/<name>.socket``.
+
+**secret**
+    This tag indicates a filename (which may be relative to |kdcdir|)
+    containing the secret used to encrypt the RADIUS packets.  The
+    secret should appear in the first line of the file by itself;
+    leading and trailing whitespace on the line will be removed.  If
+    the value of **server** is a Unix domain socket address, this tag
+    is optional, and an empty secret will be used if it is not
+    specified.  Otherwise, this tag is required.
+
+**timeout**
+    An integer which specifies the time in seconds during which the
+    KDC should attempt to contact the RADIUS server.  This tag is the
+    total time across all retries and should be less than the time
+    which an OTP value remains valid for.  The default is 5 seconds.
+
+**retries**
+    This tag specifies the number of retries to make to the RADIUS
+    server.  The default is 3 retries (4 tries).
+
+**strip_realm**
+    If this tag is ``true``, the principal without the realm will be
+    passed to the RADIUS server.  Otherwise, the realm will be
+    included.  The default value is ``true``.
+
+In the following example, requests are sent to a remote server via UDP.
+
+ ::
+
+    [otp]
+        MyRemoteTokenType = {
+            server = radius.mydomain.com:1812
+            secret = SEmfiajf42$
+            timeout = 15
+            retries = 5
+            strip_realm = true
+        }
+
+An implicit default token type named ``DEFAULT`` is defined for when
+the per-principal configuration does not specify a token type.  Its
+configuration is shown below.  You may override this token type to
+something applicable for your situation.
+
+ ::
+
+    [otp]
+        DEFAULT = {
+            strip_realm = false
+        }
+
 PKINIT options
 --------------
 
diff --git a/doc/admin/index.rst b/doc/admin/index.rst
index c40d510..3406843 100644
--- a/doc/admin/index.rst
+++ b/doc/admin/index.rst
@@ -14,6 +14,7 @@ For administrators
    host_config.rst
    backup_host.rst
    pkinit.rst
+   otp.rst
    princ_dns.rst
    enctypes.rst
 
diff --git a/doc/admin/otp.rst b/doc/admin/otp.rst
new file mode 100644
index 0000000..0abd5ff
--- /dev/null
+++ b/doc/admin/otp.rst
@@ -0,0 +1,85 @@
+OTP Preauthentication
+=====================
+
+OTP is a preauthentication mechanism for Kerberos 5 which uses One
+Time Passwords (OTP) to authenticate the client to the KDC.  The OTP
+is passed to the KDC over an encrypted FAST channel in clear-text.
+The KDC uses the password along with per-user configuration to proxy
+the request to a third-party RADIUS system.  This enables
+out-of-the-box compatibility with a large number of already widely
+deployed proprietary systems.
+
+Additionally, our implementation of the OTP system allows for the
+passing of RADIUS requests over a UNIX domain stream socket.  This
+permits the use of a local companion daemon which can handle the
+details of authentication.
+
+
+Defining token types
+--------------------
+
+Token types are defined in either krb5.conf or kdc.conf according to
+the following format::
+
+    [otp]
+        <name> = {
+            server = <host:port or filename> (default: $KDCDIR/<name>.socket)
+            secret = <filename>
+            timeout = <integer> (default: 5 [seconds])
+            retries = <integer> (default: 3)
+            strip_realm = <boolean> (default: true)
+        }
+
+If the server field begins with '/', it will be interpreted as a UNIX
+socket.  Otherwise, it is assumed to be in the format host:port.  When
+a UNIX domain socket is specified, the secret field is optional and an
+empty secret is used by default.
+
+When forwarding the request over RADIUS, by default the principal is
+used in the User-Name attribute of the RADIUS packet.  The strip_realm
+parameter controls whether the principal is forwarded with or without
+the realm portion.
+
+
+The default token type
+----------------------
+
+A default token type is used internally when no token type is specified for a
+given user.  It is defined as follows::
+
+    [otp]
+        DEFAULT = {
+            strip_realm = false
+        }
+
+The administrator may override the internal ``DEFAULT`` token type
+simply by defining a configuration with the same name.
+
+
+Token instance configuration
+----------------------------
+
+To enable OTP for a client principal, the administrator must define
+the **otp** string attribute for that principal.  The **otp** user
+string is a JSON string of the format::
+
+    [{
+        "type": <string>,
+        "username": <string>
+     }, ...]
+
+This is an array of token objects.  Both fields of token objects are
+optional.  The **type** field names the token type of this token; if
+not specified, it defaults to ``DEFAULT``.  The **username** field
+specifies the value to be sent in the User-Name RADIUS attribute.  If
+not specified, the principal name is sent, with or without realm as
+defined in the token type.
+
+For ease of configuration, an empty array (``[]``) is treated as
+equivalent to one DEFAULT token (``[{}]``).
+
+
+Other considerations
+--------------------
+
+#. FAST is required for OTP to work.
diff --git a/src/Makefile.in b/src/Makefile.in
index ab8edbd..0000510 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -14,6 +14,7 @@ SUBDIRS=util include lib \
 	plugins/pwqual/test \
 	plugins/kdb/db2 \
 	@ldap_plugin_dir@ \
+	plugins/preauth/otp \
 	plugins/preauth/pkinit \
 	kdc kadmin slave clients appl tests \
 	config-files build-tools man doc @po@
diff --git a/src/configure.in b/src/configure.in
index 2569092..b2802ba 100644
--- a/src/configure.in
+++ b/src/configure.in
@@ -1371,6 +1371,7 @@ dnl	ccapi ccapi/lib ccapi/lib/unix ccapi/server ccapi/server/unix ccapi/test
 	plugins/kdb/db2/libdb2/test
 	plugins/kdb/hdb
 	plugins/preauth/cksum_body
+	plugins/preauth/otp
 	plugins/preauth/securid_sam2
 	plugins/preauth/wpse
 	plugins/authdata/greet
diff --git a/src/kdc/kdc_preauth.c b/src/kdc/kdc_preauth.c
index c3543ca..07b180f 100644
--- a/src/kdc/kdc_preauth.c
+++ b/src/kdc/kdc_preauth.c
@@ -238,6 +238,8 @@ get_plugin_vtables(krb5_context context,
     /* Auto-register encrypted challenge and (if possible) pkinit. */
     k5_plugin_register_dyn(context, PLUGIN_INTERFACE_KDCPREAUTH, "pkinit",
                            "preauth");
+    k5_plugin_register_dyn(context, PLUGIN_INTERFACE_KDCPREAUTH, "otp",
+                           "preauth");
     k5_plugin_register(context, PLUGIN_INTERFACE_KDCPREAUTH,
                        "encrypted_challenge",
                        kdcpreauth_encrypted_challenge_initvt);
diff --git a/src/plugins/preauth/otp/Makefile.in b/src/plugins/preauth/otp/Makefile.in
new file mode 100644
index 0000000..b512c87
--- /dev/null
+++ b/src/plugins/preauth/otp/Makefile.in
@@ -0,0 +1,31 @@
+mydir=plugins$(S)preauth$(S)otp
+BUILDTOP=$(REL)..$(S)..$(S)..
+MODULE_INSTALL_DIR = $(KRB5_PA_MODULE_DIR)
+
+LIBBASE=otp
+LIBMAJOR=0
+LIBMINOR=0
+RELDIR=../plugins/preauth/otp
+
+SHLIB_EXPDEPS = $(VERTO_DEPLIBS) $(KRB5_BASE_DEPLIBS) \
+	$(TOPLIBD)/libkrad$(SHLIBEXT)
+
+SHLIB_EXPLIBS= -lkrad $(VERTO_LIBS) $(KRB5_BASE_LIBS)
+
+STLIBOBJS = \
+	otp_state.o \
+	main.o
+
+SRCS = \
+	$(srcdir)/otp_state.c \
+	$(srcdir)/main.c
+
+all-unix:: all-liblinks
+install-unix:: install-libs
+clean-unix:: clean-liblinks clean-libs clean-libobjs
+
+clean::
+	$(RM) lib$(LIBBASE)$(SO_EXT)
+
+ at libnover_frag@
+ at libobj_frag@
diff --git a/src/plugins/preauth/otp/deps b/src/plugins/preauth/otp/deps
new file mode 100644
index 0000000..68a3b25
--- /dev/null
+++ b/src/plugins/preauth/otp/deps
@@ -0,0 +1,26 @@
+#
+# Generated makefile dependencies follow.
+#
+otp_state.so otp_state.po $(OUTPRE)otp_state.$(OBJEXT): \
+  $(BUILDTOP)/include/autoconf.h $(BUILDTOP)/include/krb5/krb5.h \
+  $(BUILDTOP)/include/osconf.h $(BUILDTOP)/include/profile.h \
+  $(COM_ERR_DEPS) $(top_srcdir)/include/k5-buf.h $(top_srcdir)/include/k5-err.h \
+  $(top_srcdir)/include/k5-gmt_mktime.h $(top_srcdir)/include/k5-int-pkinit.h \
+  $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-json.h \
+  $(top_srcdir)/include/k5-platform.h $(top_srcdir)/include/k5-plugin.h \
+  $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \
+  $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \
+  $(top_srcdir)/include/krb5/plugin.h $(top_srcdir)/include/krb5/preauth_plugin.h \
+  $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \
+  otp_state.c otp_state.h
+main.so main.po $(OUTPRE)main.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \
+  $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \
+  $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(top_srcdir)/include/k5-buf.h \
+  $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h \
+  $(top_srcdir)/include/k5-int-pkinit.h $(top_srcdir)/include/k5-int.h \
+  $(top_srcdir)/include/k5-json.h $(top_srcdir)/include/k5-platform.h \
+  $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-thread.h \
+  $(top_srcdir)/include/k5-trace.h $(top_srcdir)/include/krb5.h \
+  $(top_srcdir)/include/krb5/authdata_plugin.h $(top_srcdir)/include/krb5/plugin.h \
+  $(top_srcdir)/include/krb5/preauth_plugin.h $(top_srcdir)/include/port-sockets.h \
+  $(top_srcdir)/include/socket-utils.h main.c otp_state.h
diff --git a/src/plugins/preauth/otp/main.c b/src/plugins/preauth/otp/main.c
new file mode 100644
index 0000000..2f7470e
--- /dev/null
+++ b/src/plugins/preauth/otp/main.c
@@ -0,0 +1,379 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* plugins/preauth/otp/main.c - OTP kdcpreauth module definition */
+/*
+ * Copyright 2011 NORDUnet A/S.  All rights reserved.
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "k5-int.h"
+#include "k5-json.h"
+#include <krb5/preauth_plugin.h>
+#include "otp_state.h"
+
+#include <errno.h>
+#include <ctype.h>
+
+static krb5_preauthtype otp_pa_type_list[] =
+  { KRB5_PADATA_OTP_REQUEST, 0 };
+
+struct request_state {
+    krb5_kdcpreauth_verify_respond_fn respond;
+    void *arg;
+};
+
+static krb5_error_code
+decrypt_encdata(krb5_context context, krb5_keyblock *armor_key,
+                krb5_pa_otp_req *req, krb5_data *out)
+{
+    krb5_error_code retval;
+    krb5_data plaintext;
+
+    if (req == NULL)
+        return EINVAL;
+
+    retval = alloc_data(&plaintext, req->enc_data.ciphertext.length);
+    if (retval)
+        return retval;
+
+    retval = krb5_c_decrypt(context, armor_key, KRB5_KEYUSAGE_PA_OTP_REQUEST,
+                            NULL, &req->enc_data, &plaintext);
+    if (retval != 0) {
+        com_err("otp", retval, "Unable to decrypt encData in PA-OTP-REQUEST");
+        free(plaintext.data);
+        return retval;
+    }
+
+    *out = plaintext;
+    return 0;
+}
+
+static krb5_error_code
+nonce_verify(krb5_context ctx, krb5_keyblock *armor_key,
+             const krb5_data *nonce)
+{
+    krb5_error_code retval;
+    krb5_timestamp ts;
+    krb5_data *er = NULL;
+
+    if (armor_key == NULL || nonce->data == NULL) {
+        retval = EINVAL;
+        goto out;
+    }
+
+    /* Decode the PA-OTP-ENC-REQUEST structure. */
+    retval = decode_krb5_pa_otp_enc_req(nonce, &er);
+    if (retval != 0)
+        goto out;
+
+    /* Make sure the nonce is exactly the same size as the one generated. */
+    if (er->length != armor_key->length + sizeof(krb5_timestamp))
+        goto out;
+
+    /* Check to make sure the timestamp at the beginning is still valid. */
+    ts = load_32_be(er->data);
+    retval = krb5_check_clockskew(ctx, ts);
+
+out:
+    krb5_free_data(ctx, er);
+    return retval;
+}
+
+static krb5_error_code
+timestamp_verify(krb5_context ctx, const krb5_data *nonce)
+{
+    krb5_error_code retval = EINVAL;
+    krb5_pa_enc_ts *et = NULL;
+
+    if (nonce->data == NULL)
+        goto out;
+
+    /* Decode the PA-ENC-TS-ENC structure. */
+    retval = decode_krb5_pa_enc_ts(nonce, &et);
+    if (retval != 0)
+        goto out;
+
+    /* Check the clockskew. */
+    retval = krb5_check_clockskew(ctx, et->patimestamp);
+
+out:
+    krb5_free_pa_enc_ts(ctx, et);
+    return retval;
+}
+
+static krb5_error_code
+nonce_generate(krb5_context ctx, unsigned int length, krb5_data *nonce_out)
+{
+    krb5_data nonce;
+    krb5_error_code retval;
+    krb5_timestamp now;
+
+    retval = krb5_timeofday(ctx, &now);
+    if (retval != 0)
+        return retval;
+
+    retval = alloc_data(&nonce, sizeof(now) + length);
+    if (retval != 0)
+        return retval;
+
+    retval = krb5_c_random_make_octets(ctx, &nonce);
+    if (retval != 0) {
+        free(nonce.data);
+        return retval;
+    }
+
+    store_32_be(now, nonce.data);
+    *nonce_out = nonce;
+    return 0;
+}
+
+static void
+on_response(void *data, krb5_error_code retval, otp_response response)
+{
+    struct request_state rs = *(struct request_state *)data;
+
+    free(data);
+
+    if (retval == 0 && response != otp_response_success)
+        retval = KRB5_PREAUTH_FAILED;
+
+    rs.respond(rs.arg, retval, NULL, NULL, NULL);
+}
+
+static krb5_error_code
+otp_init(krb5_context context, krb5_kdcpreauth_moddata *moddata_out,
+         const char **realmnames)
+{
+    krb5_error_code retval;
+    otp_state *state;
+
+    retval = otp_state_new(context, &state);
+    if (retval)
+        return retval;
+    *moddata_out = (krb5_kdcpreauth_moddata)state;
+    return 0;
+}
+
+static void
+otp_fini(krb5_context context, krb5_kdcpreauth_moddata moddata)
+{
+    otp_state_free((otp_state *)moddata);
+}
+
+static int
+otp_flags(krb5_context context, krb5_preauthtype pa_type)
+{
+    return PA_REPLACES_KEY;
+}
+
+static void
+otp_edata(krb5_context context, krb5_kdc_req *request,
+          krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock,
+          krb5_kdcpreauth_moddata moddata, krb5_preauthtype pa_type,
+          krb5_kdcpreauth_edata_respond_fn respond, void *arg)
+{
+    krb5_otp_tokeninfo ti, *tis[2] = { &ti, NULL };
+    krb5_keyblock *armor_key = NULL;
+    krb5_pa_otp_challenge chl;
+    krb5_pa_data *pa = NULL;
+    krb5_error_code retval;
+    krb5_data *encoding;
+    char *config;
+
+    /* Determine if otp is enabled for the user. */
+    retval = cb->get_string(context, rock, "otp", &config);
+    if (retval != 0 || config == NULL)
+        goto out;
+    cb->free_string(context, rock, config);
+
+    /* Get the armor key.  This indicates the length of random data to use in
+     * the nonce. */
+    armor_key = cb->fast_armor(context, rock);
+    if (armor_key == NULL) {
+        retval = ENOENT;
+        goto out;
+    }
+
+    /* Build the (mostly empty) challenge. */
+    memset(&ti, 0, sizeof(ti));
+    memset(&chl, 0, sizeof(chl));
+    chl.tokeninfo = tis;
+    ti.format = -1;
+    ti.length = -1;
+    ti.iteration_count = -1;
+
+    /* Generate the nonce. */
+    retval = nonce_generate(context, armor_key->length, &chl.nonce);
+    if (retval != 0)
+        goto out;
+
+    /* Build the output pa-data. */
+    retval = encode_krb5_pa_otp_challenge(&chl, &encoding);
+    if (retval != 0)
+        goto out;
+    pa = k5alloc(sizeof(krb5_pa_data), &retval);
+    if (pa == NULL) {
+        krb5_free_data(context, encoding);
+        goto out;
+    }
+    pa->pa_type = KRB5_PADATA_OTP_CHALLENGE;
+    pa->contents = (krb5_octet *)encoding->data;
+    pa->length = encoding->length;
+    free(encoding);
+
+out:
+    (*respond)(arg, retval, pa);
+}
+
+static void
+otp_verify(krb5_context context, krb5_data *req_pkt, krb5_kdc_req *request,
+           krb5_enc_tkt_part *enc_tkt_reply, krb5_pa_data *pa,
+           krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock,
+           krb5_kdcpreauth_moddata moddata,
+           krb5_kdcpreauth_verify_respond_fn respond, void *arg)
+{
+    krb5_keyblock *armor_key = NULL;
+    krb5_pa_otp_req *req = NULL;
+    struct request_state *rs;
+    krb5_error_code retval;
+    krb5_data d, plaintext;
+    char *config;
+
+    enc_tkt_reply->flags |= TKT_FLG_PRE_AUTH;
+
+    /* Get the FAST armor key. */
+    armor_key = cb->fast_armor(context, rock);
+    if (armor_key == NULL) {
+        retval = KRB5KDC_ERR_PREAUTH_FAILED;
+        com_err("otp", retval, "No armor key found when verifying padata");
+        goto error;
+    }
+
+    /* Decode the request. */
+    d = make_data(pa->contents, pa->length);
+    retval = decode_krb5_pa_otp_req(&d, &req);
+    if (retval != 0) {
+        com_err("otp", retval, "Unable to decode OTP request");
+        goto error;
+    }
+
+    /* Decrypt the nonce from the request. */
+    retval = decrypt_encdata(context, armor_key, req, &plaintext);
+    if (retval != 0) {
+        com_err("otp", retval, "Unable to decrypt nonce");
+        goto error;
+    }
+
+    /* Verify the nonce or timestamp. */
+    retval = nonce_verify(context, armor_key, &plaintext);
+    if (retval != 0)
+        retval = timestamp_verify(context, &plaintext);
+    krb5_free_data_contents(context, &plaintext);
+    if (retval != 0) {
+        com_err("otp", retval, "Unable to verify nonce or timestamp");
+        goto error;
+    }
+
+    /* Create the request state. */
+    rs = k5alloc(sizeof(struct request_state), &retval);
+    if (rs == NULL)
+        goto error;
+    rs->arg = arg;
+    rs->respond = respond;
+
+    /* Get the principal's OTP configuration string. */
+    retval = cb->get_string(context, rock, "otp", &config);
+    if (config == NULL)
+        retval = KRB5_PREAUTH_FAILED;
+    if (retval != 0) {
+        free(rs);
+        goto error;
+    }
+
+    /* Send the request. */
+    otp_state_verify((otp_state *)moddata, cb->event_context(context, rock),
+                     request->client, config, req, on_response, rs);
+    cb->free_string(context, rock, config);
+
+    k5_free_pa_otp_req(context, req);
+    return;
+
+error:
+    k5_free_pa_otp_req(context, req);
+    (*respond)(arg, retval, NULL, NULL, NULL);
+}
+
+static krb5_error_code
+otp_return_padata(krb5_context context, krb5_pa_data *padata,
+                  krb5_data *req_pkt, krb5_kdc_req *request,
+                  krb5_kdc_rep *reply, krb5_keyblock *encrypting_key,
+                  krb5_pa_data **send_pa_out, krb5_kdcpreauth_callbacks cb,
+                  krb5_kdcpreauth_rock rock, krb5_kdcpreauth_moddata moddata,
+                  krb5_kdcpreauth_modreq modreq)
+{
+    krb5_keyblock *armor_key = NULL;
+
+    if (padata->length == 0)
+        return 0;
+
+    /* Get the armor key. */
+    armor_key = cb->fast_armor(context, rock);
+    if (!armor_key) {
+      com_err("otp", ENOENT, "No armor key found when returning padata");
+      return ENOENT;
+    }
+
+    /* Replace the reply key with the FAST armor key. */
+    krb5_free_keyblock_contents(context, encrypting_key);
+    return krb5_copy_keyblock_contents(context, armor_key, encrypting_key);
+}
+
+krb5_error_code
+kdcpreauth_otp_initvt(krb5_context context, int maj_ver, int min_ver,
+                      krb5_plugin_vtable vtable);
+
+krb5_error_code
+kdcpreauth_otp_initvt(krb5_context context, int maj_ver, int min_ver,
+                      krb5_plugin_vtable vtable)
+{
+    krb5_kdcpreauth_vtable vt;
+
+    if (maj_ver != 1)
+        return KRB5_PLUGIN_VER_NOTSUPP;
+
+    vt = (krb5_kdcpreauth_vtable)vtable;
+    vt->name = "otp";
+    vt->pa_type_list = otp_pa_type_list;
+    vt->init = otp_init;
+    vt->fini = otp_fini;
+    vt->flags = otp_flags;
+    vt->edata = otp_edata;
+    vt->verify = otp_verify;
+    vt->return_padata = otp_return_padata;
+
+    com_err("otp", 0, "Loaded");
+
+    return 0;
+}
diff --git a/src/plugins/preauth/otp/otp.exports b/src/plugins/preauth/otp/otp.exports
new file mode 100644
index 0000000..26aa19d
--- /dev/null
+++ b/src/plugins/preauth/otp/otp.exports
@@ -0,0 +1 @@
+kdcpreauth_otp_initvt
diff --git a/src/plugins/preauth/otp/otp_state.c b/src/plugins/preauth/otp/otp_state.c
new file mode 100644
index 0000000..f2a64a4
--- /dev/null
+++ b/src/plugins/preauth/otp/otp_state.c
@@ -0,0 +1,649 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* plugins/preauth/otp/otp_state.c - Verify OTP token values using RADIUS */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "otp_state.h"
+
+#include <krad.h>
+#include <k5-json.h>
+
+#include <ctype.h>
+
+#ifndef HOST_NAME_MAX
+/* SUSv2 */
+#define HOST_NAME_MAX 255
+#endif
+
+#define DEFAULT_TYPE_NAME "DEFAULT"
+#define DEFAULT_SOCKET_FMT KDC_DIR "/%s.socket"
+#define DEFAULT_TIMEOUT 5
+#define DEFAULT_RETRIES 3
+#define MAX_SECRET_LEN 1024
+
+typedef struct token_type_st {
+    char *name;
+    char *server;
+    char *secret;
+    int timeout;
+    size_t retries;
+    krb5_boolean strip_realm;
+} token_type;
+
+typedef struct token_st {
+    const token_type *type;
+    krb5_data username;
+} token;
+
+typedef struct request_st {
+    otp_state *state;
+    token *tokens;
+    ssize_t index;
+    otp_cb cb;
+    void *data;
+    krad_attrset *attrs;
+} request;
+
+struct otp_state_st {
+    krb5_context ctx;
+    token_type *types;
+    krad_client *radius;
+    krad_attrset *attrs;
+};
+
+static void request_send(request *req);
+
+static krb5_error_code
+read_secret_file(const char *secret_file, char **secret)
+{
+    char buf[MAX_SECRET_LEN];
+    krb5_error_code retval;
+    char *filename;
+    FILE *file;
+    int i, j;
+
+    *secret = NULL;
+
+    retval = k5_path_join(KDC_DIR, secret_file, &filename);
+    if (retval != 0) {
+        com_err("otp", retval, "Unable to resolve secret file '%s'", filename);
+        return retval;
+    }
+
+    file = fopen(filename, "r");
+    if (file == NULL) {
+        retval = errno;
+        com_err("otp", retval, "Unable to open secret file '%s'", filename);
+        return retval;
+    }
+
+    if (fgets(buf, sizeof(buf), file) == NULL)
+        retval = EIO;
+    fclose(file);
+    if (retval != 0) {
+        com_err("otp", retval, "Unable to read secret file '%s'", filename);
+        return retval;
+    }
+
+    /* Strip whitespace. */
+    for (i = 0; buf[i] != '\0'; i++) {
+        if (!isspace(buf[i]))
+            break;
+    }
+    for (j = strlen(buf) - i; j > 0; j--) {
+        if (!isspace(buf[j - 1]))
+            break;
+    }
+
+    *secret = k5memdup0(&buf[i], j - i, &retval);
+    return retval;
+}
+
+/* Free the contents of a single token type. */
+static void
+token_type_free(token_type *type)
+{
+    if (type == NULL)
+        return;
+
+    free(type->name);
+    free(type->server);
+    free(type->secret);
+}
+
+/* Construct the internal default token type. */
+static krb5_error_code
+token_type_default(token_type *out)
+{
+    char *name = NULL, *server = NULL, *secret = NULL;
+
+    memset(out, 0, sizeof(*out));
+
+    name = strdup(DEFAULT_TYPE_NAME);
+    if (name == NULL)
+        goto oom;
+    if (asprintf(&server, DEFAULT_SOCKET_FMT, name) < 0)
+        goto oom;
+    secret = strdup("");
+    if (secret == NULL)
+        goto oom;
+
+    out->name = name;
+    out->server = server;
+    out->secret = secret;
+    out->timeout = DEFAULT_TIMEOUT * 1000;
+    out->retries = DEFAULT_RETRIES;
+    out->strip_realm = FALSE;
+    return 0;
+
+oom:
+    free(name);
+    free(server);
+    free(secret);
+    return ENOMEM;
+}
+
+/* Decode a single token type from the profile. */
+static krb5_error_code
+token_type_decode(profile_t profile, const char *name, token_type *out)
+{
+    char *server = NULL, *name_copy = NULL, *secret = NULL, *pstr = NULL;
+    int strip_realm, timeout, retries;
+    krb5_error_code retval;
+
+    memset(out, 0, sizeof(*out));
+
+    /* Set the name. */
+    name_copy = strdup(name);
+    if (name_copy == NULL)
+        return ENOMEM;
+
+    /* Set strip_realm. */
+    retval = profile_get_boolean(profile, "otp", name, "strip_realm", TRUE,
+                                 &strip_realm);
+    if (retval != 0)
+        goto cleanup;
+
+    /* Set the server. */
+    retval = profile_get_string(profile, "otp", name, "server", NULL, &pstr);
+    if (retval != 0)
+        goto cleanup;
+    if (pstr != NULL) {
+        server = strdup(pstr);
+        profile_release_string(pstr);
+        if (server == NULL) {
+            retval = ENOMEM;
+            goto cleanup;
+        }
+    } else if (asprintf(&server, DEFAULT_SOCKET_FMT, name) < 0) {
+        retval = ENOMEM;
+        goto cleanup;
+    }
+
+    /* Get the secret (optional for Unix-domain sockets). */
+    retval = profile_get_string(profile, "otp", name, "secret", NULL, &pstr);
+    if (retval != 0)
+        goto cleanup;
+    if (pstr != NULL) {
+        retval = read_secret_file(pstr, &secret);
+        profile_release_string(pstr);
+        if (retval != 0)
+            goto cleanup;
+    } else {
+        if (server[0] != '/') {
+            com_err("otp", EINVAL, "Secret missing (token type '%s')", name);
+            retval = EINVAL;
+            goto cleanup;
+        }
+
+        /* Use the default empty secret for UNIX domain stream sockets. */
+        secret = strdup("");
+        if (secret == NULL) {
+            retval = ENOMEM;
+            goto cleanup;
+        }
+    }
+
+    /* Get the timeout (profile value in seconds, result in milliseconds). */
+    retval = profile_get_integer(profile, "otp", name, "timeout",
+                                 DEFAULT_TIMEOUT, &timeout);
+    if (retval != 0)
+        goto cleanup;
+    timeout *= 1000;
+
+    /* Get the number of retries. */
+    retval = profile_get_integer(profile, "otp", name, "retries",
+                                 DEFAULT_RETRIES, &retries);
+    if (retval != 0)
+        goto cleanup;
+
+    out->name = name_copy;
+    out->server = server;
+    out->secret = secret;
+    out->timeout = timeout;
+    out->retries = retries;
+    out->strip_realm = strip_realm;
+    name_copy = server = secret = NULL;
+
+cleanup:
+    free(name_copy);
+    free(server);
+    free(secret);
+    return retval;
+}
+
+/* Free an array of token types. */
+static void
+token_types_free(token_type *types)
+{
+    size_t i;
+
+    if (types == NULL)
+        return;
+
+    for (i = 0; types[i].server != NULL; i++)
+        token_type_free(&types[i]);
+
+    free(types);
+}
+
+/* Decode an array of token types from the profile. */
+static krb5_error_code
+token_types_decode(profile_t profile, token_type **out)
+{
+    const char *hier[2] = { "otp", NULL };
+    token_type *types = NULL;
+    char **names = NULL;
+    krb5_error_code retval;
+    size_t i, pos;
+    krb5_boolean have_default = FALSE;
+
+    retval = profile_get_subsection_names(profile, hier, &names);
+    if (retval != 0)
+        return retval;
+
+    /* Check if any of the profile subsections overrides the default. */
+    for (i = 0; names[i] != NULL; i++) {
+        if (strcmp(names[i], DEFAULT_TYPE_NAME) == 0)
+            have_default = TRUE;
+    }
+
+    /* Leave space for the default (possibly) and the terminator. */
+    types = k5alloc((i + 2) * sizeof(token_type), &retval);
+    if (types == NULL)
+        goto cleanup;
+
+    /* If no default has been specified, use our internal default. */
+    pos = 0;
+    if (!have_default) {
+        retval = token_type_default(&types[pos++]);
+        if (retval != 0)
+            goto cleanup;
+    }
+
+    /* Decode each profile section into a token type element. */
+    for (i = 0; names[i] != NULL; i++) {
+        retval = token_type_decode(profile, names[i], &types[pos++]);
+        if (retval != 0)
+            goto cleanup;
+    }
+
+    *out = types;
+    types = NULL;
+
+cleanup:
+    profile_free_list(names);
+    token_types_free(types);
+    return retval;
+}
+
+/* Free the contents of a single token. */
+static void
+token_free_contents(token *t)
+{
+    if (t != NULL)
+        free(t->username.data);
+}
+
+/* Decode a single token from a JSON token object. */
+static krb5_error_code
+token_decode(krb5_context ctx, krb5_const_principal princ,
+             const token_type *types, k5_json_object obj, token *out)
+{
+    const char *typename = DEFAULT_TYPE_NAME;
+    const token_type *type = NULL;
+    char *username = NULL;
+    krb5_error_code retval;
+    k5_json_value val;
+    size_t i;
+    int flags;
+
+    memset(out, 0, sizeof(*out));
+
+    /* Find the token type. */
+    val = k5_json_object_get(obj, "type");
+    if (val != NULL && k5_json_get_tid(val) == K5_JSON_TID_STRING)
+        typename = k5_json_string_utf8(val);
+    for (i = 0; types[i].server != NULL; i++) {
+        if (strcmp(typename, types[i].name) == 0)
+            type = &types[i];
+    }
+    if (type == NULL)
+        return EINVAL;
+
+    /* Get the username, either from obj or from unparsing the principal. */
+    val = k5_json_object_get(obj, "username");
+    if (val != NULL && k5_json_get_tid(val) == K5_JSON_TID_STRING) {
+        username = strdup(k5_json_string_utf8(val));
+        if (username == NULL)
+            return ENOMEM;
+    } else {
+        flags = type->strip_realm ? KRB5_PRINCIPAL_UNPARSE_NO_REALM : 0;
+        retval = krb5_unparse_name_flags(ctx, princ, flags, &username);
+        if (retval != 0)
+            return retval;
+    }
+
+    out->type = type;
+    out->username = string2data(username);
+    return 0;
+}
+
+/* Free an array of tokens. */
+static void
+tokens_free(token *tokens)
+{
+    size_t i;
+
+    if (tokens == NULL)
+        return;
+
+    for (i = 0; tokens[i].type != NULL; i++)
+        token_free_contents(&tokens[i]);
+
+    free(tokens);
+}
+
+/* Decode a principal config string into a JSON array.  Treat an empty string
+ * or array as if it were "[{}]" which uses the default token type. */
+static krb5_error_code
+decode_config_json(const char *config, k5_json_array *out)
+{
+    krb5_error_code retval;
+    k5_json_value val;
+    k5_json_object obj;
+
+    *out = NULL;
+
+    /* Decode the config string and make sure it's an array. */
+    retval = k5_json_decode((config != NULL) ? config : "[{}]", &val);
+    if (k5_json_get_tid(val) != K5_JSON_TID_ARRAY) {
+        retval = EINVAL;
+        goto error;
+    }
+
+    /* If the array is empty, add in an empty object. */
+    if (k5_json_array_length(val) == 0) {
+        retval = k5_json_object_create(&obj);
+        if (retval != 0)
+            goto error;
+        retval = k5_json_array_add(val, obj);
+        k5_json_release(obj);
+        if (retval != 0)
+            goto error;
+    }
+
+    *out = val;
+    return 0;
+
+error:
+    k5_json_release(val);
+    return retval;
+}
+
+/* Decode an array of tokens from the configuration string. */
+static krb5_error_code
+tokens_decode(krb5_context ctx, krb5_const_principal princ,
+              const token_type *types, const char *config, token **out)
+{
+    krb5_error_code retval;
+    k5_json_array arr = NULL;
+    k5_json_value obj;
+    token *tokens = NULL;
+    size_t len, i;
+
+    retval = decode_config_json(config, &arr);
+    if (retval != 0)
+        return retval;
+    len = k5_json_array_length(arr);
+
+    tokens = k5alloc((len + 1) * sizeof(token), &retval);
+    if (tokens == NULL)
+        goto cleanup;
+
+    for (i = 0; i < len; i++) {
+        obj = k5_json_array_get(arr, i);
+        if (k5_json_get_tid(obj) != K5_JSON_TID_OBJECT) {
+            retval = EINVAL;
+            goto cleanup;
+        }
+        retval = token_decode(ctx, princ, types, obj, &tokens[i]);
+        if (retval != 0)
+            goto cleanup;
+    }
+
+    *out = tokens;
+    tokens = NULL;
+
+cleanup:
+    k5_json_release(arr);
+    tokens_free(tokens);
+    return retval;
+}
+
+static void
+request_free(request *req)
+{
+    if (req == NULL)
+        return;
+
+    krad_attrset_free(req->attrs);
+    tokens_free(req->tokens);
+    free(req);
+}
+
+krb5_error_code
+otp_state_new(krb5_context ctx, otp_state **out)
+{
+    char hostname[HOST_NAME_MAX + 1];
+    krb5_error_code retval;
+    profile_t profile;
+    krb5_data hndata;
+    otp_state *self;
+
+    retval = gethostname(hostname, sizeof(hostname));
+    if (retval != 0)
+        return retval;
+
+    self = calloc(1, sizeof(otp_state));
+    if (self == NULL)
+        return ENOMEM;
+
+    retval = krb5_get_profile(ctx, &profile);
+    if (retval != 0)
+        goto error;
+
+    retval = token_types_decode(profile, &self->types);
+    profile_abandon(profile);
+    if (retval != 0)
+        goto error;
+
+    retval = krad_attrset_new(ctx, &self->attrs);
+    if (retval != 0)
+        goto error;
+
+    hndata = make_data(hostname, strlen(hostname));
+    retval = krad_attrset_add(self->attrs,
+                              krad_attr_name2num("NAS-Identifier"), &hndata);
+    if (retval != 0)
+        goto error;
+
+    retval = krad_attrset_add_number(self->attrs,
+                                     krad_attr_name2num("Service-Type"),
+                                     KRAD_SERVICE_TYPE_AUTHENTICATE_ONLY);
+    if (retval != 0)
+        goto error;
+
+    self->ctx = ctx;
+    *out = self;
+    return 0;
+
+error:
+    otp_state_free(self);
+    return retval;
+}
+
+void
+otp_state_free(otp_state *self)
+{
+    if (self == NULL)
+        return;
+
+    krad_attrset_free(self->attrs);
+    token_types_free(self->types);
+    free(self);
+}
+
+static void
+callback(krb5_error_code retval, const krad_packet *rqst,
+         const krad_packet *resp, void *data)
+{
+    request *req = data;
+
+    req->index++;
+
+    if (retval != 0)
+        goto error;
+
+    /* If we received an accept packet, success! */
+    if (krad_packet_get_code(resp) ==
+        krad_code_name2num("Access-Accept")) {
+        req->cb(req->data, retval, otp_response_success);
+        request_free(req);
+        return;
+    }
+
+    /* If we have no more tokens to try, failure! */
+    if (req->tokens[req->index].type == NULL)
+        goto error;
+
+    /* Try the next token. */
+    request_send(req);
+
+error:
+    req->cb(req->data, retval, otp_response_fail);
+    request_free(req);
+}
+
+static void
+request_send(request *req)
+{
+    krb5_error_code retval;
+    token *tok = &req->tokens[req->index];
+    const token_type *t = tok->type;
+
+    retval = krad_attrset_add(req->attrs, krad_attr_name2num("User-Name"),
+                              &tok->username);
+    if (retval != 0)
+        goto error;
+
+    retval = krad_client_send(req->state->radius,
+                              krad_code_name2num("Access-Request"), req->attrs,
+                              t->server, t->secret, t->timeout, t->retries,
+                              callback, req);
+    krad_attrset_del(req->attrs, krad_attr_name2num("User-Name"), 0);
+    if (retval != 0)
+        goto error;
+
+    return;
+
+error:
+    req->cb(req->data, retval, otp_response_fail);
+    request_free(req);
+}
+
+void
+otp_state_verify(otp_state *state, verto_ctx *ctx, krb5_const_principal princ,
+                 const char *config, const krb5_pa_otp_req *req,
+                 otp_cb cb, void *data)
+{
+    krb5_error_code retval;
+    request *rqst = NULL;
+    char *name;
+
+    if (state->radius == NULL) {
+        retval = krad_client_new(state->ctx, ctx, &state->radius);
+        if (retval != 0)
+            goto error;
+    }
+
+    rqst = calloc(1, sizeof(request));
+    if (rqst == NULL) {
+        (*cb)(data, ENOMEM, otp_response_fail);
+        return;
+    }
+    rqst->state = state;
+    rqst->data = data;
+    rqst->cb = cb;
+
+    retval = krad_attrset_copy(state->attrs, &rqst->attrs);
+    if (retval != 0)
+        goto error;
+
+    retval = krad_attrset_add(rqst->attrs, krad_attr_name2num("User-Password"),
+                              &req->otp_value);
+    if (retval != 0)
+        goto error;
+
+    retval = tokens_decode(state->ctx, princ, state->types, config,
+                           &rqst->tokens);
+    if (retval != 0) {
+        if (krb5_unparse_name(state->ctx, princ, &name) == 0) {
+            com_err("otp", retval,
+                    "Can't decode otp config string for principal '%s'", name);
+            krb5_free_unparsed_name(state->ctx, name);
+        }
+        goto error;
+    }
+
+    request_send(rqst);
+    return;
+
+error:
+    (*cb)(data, retval, otp_response_fail);
+    request_free(rqst);
+}
diff --git a/src/plugins/preauth/otp/otp_state.h b/src/plugins/preauth/otp/otp_state.h
new file mode 100644
index 0000000..4247d0b
--- /dev/null
+++ b/src/plugins/preauth/otp/otp_state.h
@@ -0,0 +1,59 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* plugins/preauth/otp/otp_state.h - Internal declarations for OTP module */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in
+ *       the documentation and/or other materials provided with the
+ *       distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+ * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef OTP_H_
+#define OTP_H_
+
+#include <k5-int.h>
+#include <verto.h>
+
+#include <com_err.h>
+
+typedef enum otp_response {
+    otp_response_fail = 0,
+    otp_response_success
+    /* Other values reserved for responses like next token or new pin. */
+} otp_response;
+
+typedef struct otp_state_st otp_state;
+typedef void
+(*otp_cb)(void *data, krb5_error_code retval, otp_response response);
+
+krb5_error_code
+otp_state_new(krb5_context ctx, otp_state **self);
+
+void
+otp_state_free(otp_state *self);
+
+void
+otp_state_verify(otp_state *state, verto_ctx *ctx, krb5_const_principal princ,
+                 const char *config, const krb5_pa_otp_req *request,
+                 otp_cb cb, void *data);
+
+#endif /* OTP_H_ */
diff --git a/src/tests/Makefile.in b/src/tests/Makefile.in
index a7f8c2d..bf09738 100644
--- a/src/tests/Makefile.in
+++ b/src/tests/Makefile.in
@@ -87,6 +87,7 @@ check-pytests:: gcred hist kdbtest plugorder t_init_creds t_localauth
 	$(RUNPYTEST) $(srcdir)/t_anonpkinit.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_authpkinit.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_policy.py $(PYTESTFLAGS)
+	$(RUNPYTEST) $(srcdir)/t_otp.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_localauth.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_kadm5_hook.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_pwqual.py $(PYTESTFLAGS)
diff --git a/src/tests/t_otp.py b/src/tests/t_otp.py
new file mode 100644
index 0000000..66a03ee
--- /dev/null
+++ b/src/tests/t_otp.py
@@ -0,0 +1,226 @@
+#!/usr/bin/python
+#
+# Author: Nathaniel McCallum <npmccallum at redhat.com>
+#
+# Copyright (c) 2013 Red Hat, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+
+#
+# This script tests OTP, both UDP and Unix Sockets, with a variety of
+# configuration. It requires pyrad to run, but exits gracefully if not found.
+# It also deliberately shuts down the test daemons between tests in order to
+# test how OTP handles the case of short daemon restarts.
+#
+
+from Queue import Empty
+import StringIO
+import struct
+import subprocess
+import sys
+import socket
+import os
+import atexit
+
+try:
+    from pyrad import packet, dictionary
+    from multiprocessing import Process, Queue
+except ImportError:
+    success('Warning: skipping OTP tests due to missing pyrad or old Python')
+    exit(0)
+
+from k5test import *
+
+class RadiusDaemon(Process):
+    MAX_PACKET_SIZE = 4096
+
+    # We could use a dictionary file, but since we need
+    # such few attributes, we'll just include them here
+    DICTIONARY = dictionary.Dictionary(StringIO.StringIO("""
+ATTRIBUTE    User-Name    1    string
+ATTRIBUTE    User-Password   2    string
+ATTRIBUTE    NAS-Identifier  32    string
+"""))
+
+    def listen(self, addr):
+        raise NotImplementedError()
+
+    def recvRequest(self, data):
+        raise NotImplementedError()
+
+    def run(self):
+        addr = self._args[0]
+        secr = self._args[1]
+        pswd = self._args[2]
+        outq = self._args[3]
+
+        if secr:
+            with open(secr) as file:
+                secr = file.read().strip()
+
+        data = self.listen(addr)
+        outq.put("started")
+        (buf, sock, addr) = self.recvRequest(data)
+        pkt = packet.AuthPacket(secret=secr,
+                                dict=RadiusDaemon.DICTIONARY,
+                                packet=buf)
+
+        usernm = []
+        passwd = []
+        for key in pkt.keys():
+            if key == 'User-Password':
+                passwd = map(pkt.PwDecrypt, pkt[key])
+            elif key == 'User-Name':
+                usernm = pkt[key]
+
+        reply = pkt.CreateReply()
+        replyq = {'user': usernm, 'pass': passwd}
+        if passwd == [pswd]:
+            reply.code = packet.AccessAccept
+            replyq['reply'] = True
+        else:
+            reply.code = packet.AccessReject
+            replyq['reply'] = False
+
+        outq.put(replyq)
+        if addr is None:
+            sock.send(reply.ReplyPacket())
+        else:
+            sock.sendto(reply.ReplyPacket(), addr)
+        sock.close()
+
+class UDPRadiusDaemon(RadiusDaemon):
+    def listen(self, addr):
+        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        sock.bind((addr.split(':')[0], int(addr.split(':')[1])))
+        return sock
+
+    def recvRequest(self, sock):
+        (buf, addr) = sock.recvfrom(RadiusDaemon.MAX_PACKET_SIZE)
+        return (buf, sock, addr)
+
+class UnixRadiusDaemon(RadiusDaemon):
+    def listen(self, addr):
+        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        if os.path.exists(addr):
+            os.remove(addr)
+        sock.bind(addr)
+        sock.listen(1)
+        return (sock, addr)
+
+    def recvRequest(self, (sock, addr)):
+        conn = sock.accept()[0]
+        sock.close()
+        os.remove(addr)
+
+        buf = ""
+        remain = RadiusDaemon.MAX_PACKET_SIZE
+        while True:
+            buf += conn.recv(remain)
+            remain = RadiusDaemon.MAX_PACKET_SIZE - len(buf)
+            if (len(buf) >= 4):
+                remain = struct.unpack("!BBH", buf[0:4])[2] - len(buf)
+                if (remain <= 0):
+                    return (buf, conn, None)
+
+def verify(daemon, queue, reply, usernm, passwd):
+    try:
+        data = queue.get(timeout=1)
+    except Empty:
+        sys.stderr.write("ERROR: Packet not received by daemon!\n")
+        daemon.terminate()
+        sys.exit(1)
+    assert data['reply'] is reply
+    assert data['user'] == [usernm]
+    assert data['pass'] == [passwd]
+    daemon.join()
+
+def setstr(princ, type, username=None):
+    cmd = 'setstr %s otp "[{""type"": ""%s""' % (princ, type)
+    if username is None:
+        cmd += '}]"'
+    else:
+        cmd += ', ""username"": ""%s""}]"' % username
+    return cmd
+
+prefix = "/tmp/%d" % os.getpid()
+secret_file = prefix + ".secret"
+socket_file = prefix + ".socket"
+with open(secret_file, "w") as file:
+    file.write("otptest")
+atexit.register(lambda: os.remove(secret_file))
+
+conf = {'plugins': {'kdcpreauth': {'enable_only': 'otp'}},
+        'otp': {'udp': {'server': '127.0.0.1:$port9',
+                        'secret': secret_file,
+                        'strip_realm': 'true'},
+                'unix': {'server': socket_file,
+                         'strip_realm': 'false'}}}
+
+queue = Queue()
+
+realm = K5Realm(kdc_conf=conf)
+realm.run_kadminl('modprinc +requires_preauth %s' % realm.user_princ)
+flags = ['-T', realm.ccache]
+server_addr = '127.0.0.1:' + str(realm.portbase + 9)
+
+## Test UDP fail / custom username
+daemon = UDPRadiusDaemon(args=(server_addr, secret_file, 'accept', queue))
+daemon.start()
+queue.get()
+realm.run_kadminl(setstr(realm.user_princ, 'udp', 'custom'))
+realm.kinit(realm.user_princ, 'reject', flags=flags, expected_code=1)
+verify(daemon, queue, False, 'custom', 'reject')
+
+## Test UDP success / standard username
+daemon = UDPRadiusDaemon(args=(server_addr, secret_file, 'accept', queue))
+daemon.start()
+queue.get()
+realm.run_kadminl(setstr(realm.user_princ, 'udp'))
+realm.kinit(realm.user_princ, 'accept', flags=flags)
+verify(daemon, queue, True, realm.user_princ.split('@')[0], 'accept')
+
+# Detect upstream pyrad bug
+#   https://github.com/wichert/pyrad/pull/18
+try:
+    auth = packet.Packet.CreateAuthenticator()
+    packet.Packet(authenticator=auth, secret="").ReplyPacket()
+except AssertionError:
+    success('Warning: skipping UNIX domain socket tests because of pyrad '
+            'assertion bug')
+    exit(0)
+
+## Test Unix fail / custom username
+daemon = UnixRadiusDaemon(args=(socket_file, '', 'accept', queue))
+daemon.start()
+queue.get()
+realm.run_kadminl(setstr(realm.user_princ, 'unix', 'custom'))
+realm.kinit(realm.user_princ, 'reject', flags=flags, expected_code=1)
+verify(daemon, queue, False, 'custom', 'reject')
+
+## Test Unix success / standard username
+daemon = UnixRadiusDaemon(args=(socket_file, '', 'accept', queue))
+daemon.start()
+queue.get()
+realm.run_kadminl(setstr(realm.user_princ, 'unix'))
+realm.kinit(realm.user_princ, 'accept', flags=flags)
+verify(daemon, queue, True, realm.user_princ, 'accept')
+
+success('OTP tests')


More information about the cvs-krb5 mailing list