]> git.ipfire.org Git - thirdparty/krb5.git/commitdiff
Add server-side otp preauth plugin
authorNathaniel McCallum <npmccallum@redhat.com>
Wed, 3 Apr 2013 16:38:05 +0000 (12:38 -0400)
committerGreg Hudson <ghudson@mit.edu>
Thu, 11 Jul 2013 18:14:34 +0000 (14:14 -0400)
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

14 files changed:
doc/admin/conf_files/kdc_conf.rst
doc/admin/index.rst
doc/admin/otp.rst [new file with mode: 0644]
src/Makefile.in
src/configure.in
src/kdc/kdc_preauth.c
src/plugins/preauth/otp/Makefile.in [new file with mode: 0644]
src/plugins/preauth/otp/deps [new file with mode: 0644]
src/plugins/preauth/otp/main.c [new file with mode: 0644]
src/plugins/preauth/otp/otp.exports [new file with mode: 0644]
src/plugins/preauth/otp/otp_state.c [new file with mode: 0644]
src/plugins/preauth/otp/otp_state.h [new file with mode: 0644]
src/tests/Makefile.in
src/tests/t_otp.py [new file with mode: 0644]

index c7007d647b23ef0fc2d44f01d9c984ccf8c9f9a0..3b56e61e82740ccba51645ce2baee40ba14f0212 100644 (file)
@@ -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
 --------------
 
index c40d51016c2213c5770842057e8d418a7d65e80b..3406843b10d23f05fc347fbceb8c56c5d116989d 100644 (file)
@@ -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 (file)
index 0000000..0abd5ff
--- /dev/null
@@ -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.
index ab8edbd89e52684685d22cfb5e8ede4b88024089..0000510d427ec4635079fce55aabd371706d346a 100644 (file)
@@ -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@
index 2569092a917a8b27cef87cf9300e5d22369f63bb..b2802baee9dd5af3a55bc4d283074fa7203579ad 100644 (file)
@@ -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
index c3543caaecbc41365c55a9625ca1ad9e549533e0..07b180f28cc7798d9cc64169dbeadbde5ee2a636 100644 (file)
@@ -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 (file)
index 0000000..b512c87
--- /dev/null
@@ -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)
+
+@libnover_frag@
+@libobj_frag@
diff --git a/src/plugins/preauth/otp/deps b/src/plugins/preauth/otp/deps
new file mode 100644 (file)
index 0000000..68a3b25
--- /dev/null
@@ -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 (file)
index 0000000..2f7470e
--- /dev/null
@@ -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 (file)
index 0000000..26aa19d
--- /dev/null
@@ -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 (file)
index 0000000..f2a64a4
--- /dev/null
@@ -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 (file)
index 0000000..4247d0b
--- /dev/null
@@ -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_ */
index a7f8c2d413d5da0e59c20bb7ad1efa2bc80624f7..bf097387eae4f149292396f60f06c6f6824facda 100644 (file)
@@ -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 (file)
index 0000000..66a03ee
--- /dev/null
@@ -0,0 +1,226 @@
+#!/usr/bin/python
+#
+# Author: Nathaniel McCallum <npmccallum@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')