[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]

Re: [Freeipa-devel] [PATCH] krb 1.12's OTP-Over-RADIUS



On Wed, 2013-03-06 at 13:04 -0500, Nathaniel McCallum wrote:
> On Wed, 2013-03-06 at 12:56 -0500, Nathaniel McCallum wrote:
> > Patch is attached.
> > 
> > There are currently a few security downsides to this patch:
> > 1. The daemon (ipa-otpd) runs as root and binds anonymously
> > 2. ipatokenRadiusSecret is readable by an anonymous bind
> > 
> > This patch also adds some new dependencies, namely:
> > 1. libverto (a dependency of krb5)
> > 2. systemd
> > 3. a krb5 patched for libk5radius support [1]
> > 
> > In the interest of trying to meet the Fedora Features deadline, I am
> > providing the patch in spite of the above issues.
> > 
> > Nathaniel
> > 
> > 1 - http://bit.ly/ZqtK79
> 
> Also, I assumed the usability of 2.16.840.1.113730.3.8.16 for the
> schema. This will need to be verified and finalized.

Updated version of the patch attached. Requires libk5radius from here:
https://github.com/npmccallum/krb5/commits/otp

This new version fixes a bug which caused a hang in the case of no entry
found during LDAP query.

Nathaniel

>From d458c4d8c6feba227b082a21b97d2f6f696ea9a1 Mon Sep 17 00:00:00 2001
From: Nathaniel McCallum <nathaniel themccallums org>
Date: Wed, 6 Mar 2013 17:32:42 -0500
Subject: [PATCH] Add support for krb5 1.12's OTP-Over-RADIUS

MIT krb5 1.12 is adding support for OTP tokens over RADIUS. This
can be accomplished in a zero configuration environment via a
well known UNIX domain socket. This patch adds a schema
(70ipaotp.ldif) which provides OTP support to FreeIPA and a
daemon (ipa-otpd) that listens for krb5 OTP requests. However,
this commit does not provide a UI for managing tokens.
---
 daemons/Makefile.am                   |   1 +
 daemons/configure.ac                  |  97 ++++------
 daemons/ipa-otpd/Makefile.am          |  21 +++
 daemons/ipa-otpd/bind.c               | 143 +++++++++++++++
 daemons/ipa-otpd/forward.c            | 118 +++++++++++++
 daemons/ipa-otpd/internal.h           | 162 +++++++++++++++++
 daemons/ipa-otpd/ipa-otpd.socket.in   |   8 +
 daemons/ipa-otpd/ipa-otpd  service in |   9 +
 daemons/ipa-otpd/main.c               | 322 ++++++++++++++++++++++++++++++++++
 daemons/ipa-otpd/parse.c              | 168 ++++++++++++++++++
 daemons/ipa-otpd/query.c              | 244 ++++++++++++++++++++++++++
 daemons/ipa-otpd/queue.c              | 207 ++++++++++++++++++++++
 daemons/ipa-otpd/stdio.c              | 190 ++++++++++++++++++++
 daemons/ipa-otpd/test.py              |  61 +++++++
 install/share/70ipaotp.ldif           | 159 +++++++++++++++++
 install/share/Makefile.am             |   1 +
 install/share/copy-schema-to-ca.py    |   1 +
 ipaserver/install/dsinstance.py       |   3 +-
 18 files changed, 1854 insertions(+), 61 deletions(-)
 create mode 100644 daemons/ipa-otpd/Makefile.am
 create mode 100644 daemons/ipa-otpd/bind.c
 create mode 100644 daemons/ipa-otpd/forward.c
 create mode 100644 daemons/ipa-otpd/internal.h
 create mode 100644 daemons/ipa-otpd/ipa-otpd.socket.in
 create mode 100644 daemons/ipa-otpd/ipa-otpd  service in
 create mode 100644 daemons/ipa-otpd/main.c
 create mode 100644 daemons/ipa-otpd/parse.c
 create mode 100644 daemons/ipa-otpd/query.c
 create mode 100644 daemons/ipa-otpd/queue.c
 create mode 100644 daemons/ipa-otpd/stdio.c
 create mode 100644 daemons/ipa-otpd/test.py
 create mode 100644 install/share/70ipaotp.ldif

diff --git a/daemons/Makefile.am b/daemons/Makefile.am
index 05cd1a7..956f399 100644
--- a/daemons/Makefile.am
+++ b/daemons/Makefile.am
@@ -16,6 +16,7 @@ SUBDIRS =			\
 	ipa-kdb			\
 	ipa-slapi-plugins	\
 	ipa-sam			\
+	ipa-otpd		\
 	$(NULL)
 
 DISTCLEANFILES =		\
diff --git a/daemons/configure.ac b/daemons/configure.ac
index ae57d64..071f87b 100644
--- a/daemons/configure.ac
+++ b/daemons/configure.ac
@@ -79,63 +79,17 @@ dnl ---------------------------------------------------------------------------
 dnl - Check for KRB5
 dnl ---------------------------------------------------------------------------
 
-KRB5_LIBS=
 AC_CHECK_HEADER(krb5.h, [], [AC_MSG_ERROR([krb5.h not found])])
-
-krb5_impl=mit
-
-if test "x$ac_cv_header_krb5_h" = "xyes" ; then
-  dnl lazy check for Heimdal Kerberos
-  AC_CHECK_HEADERS(heim_err.h)
-  if test $ac_cv_header_heim_err_h = yes ; then
-    krb5_impl=heimdal
-  else
-    krb5_impl=mit
-  fi
-
-  if test "x$krb5_impl" = "xmit"; then
-    AC_CHECK_LIB(k5crypto, main,
-      [krb5crypto=k5crypto],
-      [krb5crypto=crypto])
-
-    AC_CHECK_LIB(krb5, main,
-      [have_krb5=yes
-	KRB5_LIBS="-lkrb5 -l$krb5crypto -lcom_err"],
-      [have_krb5=no],
-      [-l$krb5crypto -lcom_err])
-
-  elif test "x$krb5_impl" = "xheimdal"; then
-    AC_CHECK_LIB(des, main,
-      [krb5crypto=des],
-      [krb5crypto=crypto])
-
-    AC_CHECK_LIB(krb5, main,
-      [have_krb5=yes
-	KRB5_LIBS="-lkrb5 -l$krb5crypto -lasn1 -lroken -lcom_err"],
-      [have_krb5=no],
-      [-l$krb5crypto -lasn1 -lroken -lcom_err])
-
-    AC_DEFINE(HAVE_HEIMDAL_KERBEROS, 1,
-      [define if you have HEIMDAL Kerberos])
-
-  else
-    have_krb5=no
-    AC_MSG_WARN([Unrecognized Kerberos5 Implementation])
-  fi
-
-  if test "x$have_krb5" = "xyes" ; then
-    ol_link_krb5=yes
-
-    AC_DEFINE(HAVE_KRB5, 1,
-      [define if you have Kerberos V])
-
-  else
-    AC_MSG_ERROR([Required Kerberos 5 support not available])
-  fi
-
-fi
-
+AC_CHECK_HEADER(k5radius.h, [], [AC_MSG_ERROR([k5radius.h not found])])
+AC_CHECK_LIB(krb5, main, [], [AC_MSG_ERROR([libkrb5 not found])])
+AC_CHECK_LIB(k5crypto, main, [krb5crypto=k5crypto], [krb5crypto=crypto])
+AC_CHECK_LIB(k5radius, main, [], [AC_MSG_ERROR([libk5radius not found])])
+KRB5_LIBS="-lkrb5 -l$krb5crypto -lcom_err"
+K5RADIUS_LIBS="-lk5radius"
+krb5kdcdir="${localstatedir}/kerberos/krb5kdc"
 AC_SUBST(KRB5_LIBS)
+AC_SUBST(K5RADIUS_LIBS)
+AC_SUBST(krb5kdcdir)
 
 dnl ---------------------------------------------------------------------------
 dnl - Check for Mozilla LDAP and OpenLDAP SDK
@@ -253,6 +207,11 @@ AC_CHECK_LIB([wbclient],
 AC_SUBST(WBCLIENT_LIBS)
 
 dnl ---------------------------------------------------------------------------
+dnl Check for libverto
+dnl ---------------------------------------------------------------------------
+PKG_CHECK_MODULES([LIBVERTO], [libverto])
+
+dnl ---------------------------------------------------------------------------
 dnl - Check for check unit test framework http://check.sourceforge.net/
 dnl ---------------------------------------------------------------------------
 PKG_CHECK_MODULES([CHECK], [check >= 0.9.5], [have_check=1], [have_check=])
@@ -269,6 +228,20 @@ dnl -- sss_idmap is needed by the extdom exop --
 PKG_CHECK_MODULES([SSSIDMAP], [sss_idmap])
 
 dnl ---------------------------------------------------------------------------
+dnl - Check for systemd unit directory
+dnl ---------------------------------------------------------------------------
+PKG_CHECK_EXISTS([systemd], [], [AC_MSG_ERROR([systemd not found])])
+AC_ARG_WITH([systemdsystemunitdir],
+            AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd service files]),
+            [], [with_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd)])
+AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir])
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for program paths
+dnl ---------------------------------------------------------------------------
+AC_PATH_PROG(UNLINK, unlink, [AC_MSG_ERROR([unlink not found])])
+
+dnl ---------------------------------------------------------------------------
 dnl - Set the data install directory since we don't use pkgdatadir
 dnl ---------------------------------------------------------------------------
 
@@ -332,6 +305,7 @@ AC_CONFIG_FILES([
     Makefile
     ipa-kdb/Makefile
     ipa-sam/Makefile
+    ipa-otpd/Makefile
     ipa-slapi-plugins/Makefile
     ipa-slapi-plugins/ipa-cldap/Makefile
     ipa-slapi-plugins/ipa-enrollment/Makefile
@@ -352,19 +326,22 @@ echo "
                     IPA Server $VERSION
                     ========================
 
-	prefix:                   ${prefix}
-	exec_prefix:              ${exec_prefix}
+        prefix:                   ${prefix}
+        exec_prefix:              ${exec_prefix}
         libdir:                   ${libdir}
         bindir:                   ${bindir}
         sbindir:                  ${sbindir}
         sysconfdir:               ${sysconfdir}
         localstatedir:            ${localstatedir}
         datadir:                  ${datadir}
-	source code location:	  ${srcdir}
-	compiler:		  ${CC}
-	cflags:		          ${CFLAGS}
+        krb5kdcdir:               ${krb5kdcdir}
+        systemdsystemunitdir:     ${systemdsystemunitdir}
+        source code location:     ${srcdir}
+        compiler:                 ${CC}
+        cflags:		          ${CFLAGS}
         LDAP libs:                ${LDAP_LIBS}
         KRB5 libs:                ${KRB5_LIBS}
+        K5RADIUS libs:            ${K5RADIUS_LIBS}
         OpenSSL libs:             ${SSL_LIBS}
         Maintainer mode:          ${USE_MAINTAINER_MODE}
 "
diff --git a/daemons/ipa-otpd/Makefile.am b/daemons/ipa-otpd/Makefile.am
new file mode 100644
index 0000000..4f3aaf5
--- /dev/null
+++ b/daemons/ipa-otpd/Makefile.am
@@ -0,0 +1,21 @@
+CFLAGS := $(CFLAGS) @LDAP_CFLAGS@ @LIBVERTO_CFLAGS@
+LDFLAGS := $(LDFLAGS) @LDAP_LIBS@ @LIBVERTO_LIBS@ @KRB5_LIBS@ @K5RADIUS_LIBS@
+
+noinst_HEADERS = internal.h
+libexec_PROGRAMS = ipa-otpd
+dist_noinst_DATA = ipa-otpd.socket.in ipa-otpd  service in test.py
+systemdsystemunit_DATA = ipa-otpd.socket ipa-otpd  service
+
+ipa_otpd_SOURCES = bind.c forward.c main.c parse.c query.c queue.c stdio.c
+
+%.socket: %.socket.in
+	@sed -e 's|@krb5kdcdir[ ]|$(krb5kdcdir)|g' \
+	     -e 's|@UNLINK[ ]|@UNLINK@|g' \
+	     $< > $@
+
+%.service: %.service.in
+	@sed -e 's|@libexecdir[ ]|$(libexecdir)|g' \
+	     -e 's|@sysconfdir[ ]|$(sysconfdir)|g' \
+	     $< > $@
+
+CLEANFILES = $(systemdsystemunit_DATA)
diff --git a/daemons/ipa-otpd/bind.c b/daemons/ipa-otpd/bind.c
new file mode 100644
index 0000000..ed2f725
--- /dev/null
+++ b/daemons/ipa-otpd/bind.c
@@ -0,0 +1,143 @@
+/*
+ * FreeIPA 2FA companion daemon
+ *
+ * Authors: Nathaniel McCallum <npmccallum redhat com>
+ *
+ * Copyright (C) 2013  Nathaniel McCallum, Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "internal.h"
+
+void
+on_bind_writable(verto_ctx *vctx, verto_ev *ev)
+{
+    queue *push = &ctx.stdio.responses;
+    const krb5_data *data;
+    struct berval cred;
+    item *itm;
+    int i;
+    (void)vctx;
+
+    itm = queue_pop(&ctx.bind.requests);
+    if (itm == NULL) {
+        verto_set_flags(ctx.bind.io, VERTO_EV_FLAG_PERSIST |
+                                     VERTO_EV_FLAG_IO_ERROR |
+                                     VERTO_EV_FLAG_IO_READ);
+        return;
+    }
+
+    if (itm->user.dn == NULL)
+        goto error;
+
+    data = k5_radius_packet_get_attr(itm->req,
+                                     k5_radius_attr_name2num("User-Password"),
+                                     0);
+    if (data == NULL)
+        goto error;
+
+    cred.bv_val = data->data;
+    cred.bv_len = data->length;
+    i = ldap_sasl_bind(verto_get_private(ev), itm->user.dn, LDAP_SASL_SIMPLE,
+                       &cred, NULL, NULL, &itm->msgid);
+    if (i != LDAP_SUCCESS) {
+        log_err(errno, "Unable to initiate bind: %s", ldap_err2string(i));
+        verto_break(ctx.vctx);
+        ctx.exitstatus = 1;
+    }
+
+    log_req(itm->req, "bind start: %s", itm->user.dn);
+    push = &ctx.bind.responses;
+
+error:
+    queue_push(push, itm);
+}
+
+void
+on_bind_readable(verto_ctx *vctx, verto_ev *ev)
+{
+    const char *errstr = "error";
+    LDAPMessage *results;
+    item *itm = NULL;
+    int i, rslt;
+    (void)vctx;
+
+    rslt = ldap_result(verto_get_private(ev), LDAP_RES_ANY, 0, NULL, &results);
+    if (rslt != LDAP_RES_BIND) {
+        if (rslt <= 0)
+            results = NULL;
+        ldap_msgfree(results);
+        return;
+    }
+
+    itm = queue_pop_msgid(&ctx.bind.responses, ldap_msgid(results));
+    if (itm == NULL) {
+        ldap_msgfree(results);
+        return;
+    }
+    itm->msgid = -1;
+
+    rslt = ldap_parse_result(verto_get_private(ev), results, &i,
+                             NULL, NULL, NULL, NULL, 0);
+    if (rslt != LDAP_SUCCESS) {
+        errstr = ldap_err2string(rslt);
+        goto error;
+    }
+
+    rslt = i;
+    if (rslt != LDAP_SUCCESS) {
+        errstr = ldap_err2string(rslt);
+        goto error;
+    }
+
+    itm->sent = 0;
+    i = k5_radius_packet_new_response(ctx.kctx, SECRET,
+                                      k5_radius_code_name2num("Access-Accept"),
+                                      NULL, itm->req, &itm->rsp);
+    if (i != 0) {
+        errstr = krb5_get_error_message(ctx.kctx, i);
+        goto error;
+    }
+
+error:
+    if (itm != NULL)
+        log_req(itm->req, "bind end: %s",
+                itm->rsp != NULL ? "success" : errstr);
+
+    ldap_msgfree(results);
+    queue_push(&ctx.stdio.responses, itm);
+    verto_set_flags(ctx.stdio.writer, VERTO_EV_FLAG_PERSIST |
+                                      VERTO_EV_FLAG_IO_ERROR |
+                                      VERTO_EV_FLAG_IO_READ |
+                                      VERTO_EV_FLAG_IO_WRITE);
+}
+
+void
+on_bind_io(verto_ctx *vctx, verto_ev *ev)
+{
+    verto_ev_flag flags;
+
+    flags = verto_get_fd_state(ev);
+    if (flags & VERTO_EV_FLAG_IO_WRITE)
+        on_bind_writable(vctx, ev);
+    if (flags & VERTO_EV_FLAG_IO_READ)
+        on_bind_readable(vctx, ev);
+    if (flags & VERTO_EV_FLAG_IO_ERROR) {
+        log_err(EIO, "IO error received on bind socket");
+        verto_break(ctx.vctx);
+        ctx.exitstatus = 1;
+    }
+}
diff --git a/daemons/ipa-otpd/forward.c b/daemons/ipa-otpd/forward.c
new file mode 100644
index 0000000..b0c9530
--- /dev/null
+++ b/daemons/ipa-otpd/forward.c
@@ -0,0 +1,118 @@
+/*
+ * FreeIPA 2FA companion daemon
+ *
+ * Authors: Nathaniel McCallum <npmccallum redhat com>
+ *
+ * Copyright (C) 2013  Nathaniel McCallum, Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "internal.h"
+
+static void
+forward_cb(krb5_error_code retval,
+           const k5_radius_packet *request,
+           const k5_radius_packet *response,
+           void *data)
+{
+    k5_radius_code code, acpt;
+    item *itm = data;
+    (void)request;
+
+    acpt = k5_radius_code_name2num("Access-Accept");
+    code = k5_radius_packet_get_code(response);
+    if (retval == 0 && code == acpt) {
+        itm->sent = 0;
+        retval = k5_radius_packet_new_response(ctx.kctx, SECRET, acpt,
+                                               NULL, itm->req, &itm->rsp);
+    }
+
+    log_req(itm->req, "forward end: %s",
+            retval == 0
+              ? k5_radius_code_num2name(code)
+              : krb5_get_error_message(ctx.kctx, retval));
+
+    queue_push(&ctx.stdio.responses, itm);
+    verto_set_flags(ctx.stdio.writer, VERTO_EV_FLAG_PERSIST |
+                                      VERTO_EV_FLAG_IO_ERROR |
+                                      VERTO_EV_FLAG_IO_READ |
+                                      VERTO_EV_FLAG_IO_WRITE);
+}
+
+krb5_error_code
+forward(item **i)
+{
+    krb5_error_code retval;
+    k5_radius_attr usernameid, passwordid;
+    char *username;
+    krb5_data tmp;
+
+    /* Find the username. */
+    username = (*i)->user.ipatokenRadiusUserName;
+    if (username == NULL) {
+      username = (*i)->user.other;
+      if (username == NULL)
+          username = (*i)->user.uid;
+    }
+
+    /* Check to see if we are supposed to forward. */
+    if ((*i)->radius.ipatokenRadiusServer == NULL ||
+        (*i)->radius.ipatokenRadiusSecret == NULL ||
+        username == NULL)
+        return 0;
+
+    log_req((*i)->req, "forward start: %s / %s", username,
+            (*i)->radius.ipatokenRadiusServer);
+
+    usernameid = k5_radius_attr_name2num("User-Name");
+    passwordid = k5_radius_attr_name2num("User-Password");
+
+    /* Set User-Name. */
+    tmp.data = username;
+    tmp.length = strlen(tmp.data);
+    retval = k5_radius_attrset_add(ctx.attrs, usernameid, &tmp);
+    if (retval != 0)
+        goto error;
+
+    /* Set User-Password. */
+    retval = k5_radius_attrset_add(ctx.attrs, passwordid,
+                                   k5_radius_packet_get_attr((*i)->req,
+                                                             passwordid, 0));
+    if (retval != 0) {
+        k5_radius_attrset_del(ctx.attrs, usernameid, 0);
+        goto error;
+    }
+
+    /* Forward the request to the RADIUS server. */
+    retval = k5_radius_client_send(ctx.client,
+                                   k5_radius_code_name2num("Access-Request"),
+                                   ctx.attrs,
+                                   (*i)->radius.ipatokenRadiusServer,
+                                   (*i)->radius.ipatokenRadiusSecret,
+                                   (*i)->radius.ipatokenRadiusTimeout,
+                                   (*i)->radius.ipatokenRadiusRetries,
+                                   forward_cb, *i);
+    k5_radius_attrset_del(ctx.attrs, usernameid, 0);
+    k5_radius_attrset_del(ctx.attrs, passwordid, 0);
+    if (retval == 0)
+        *i = NULL;
+
+error:
+    if (retval != 0)
+        log_req((*i)->req, "forward end: %s",
+                krb5_get_error_message(ctx.kctx, retval));
+    return retval;
+}
diff --git a/daemons/ipa-otpd/internal.h b/daemons/ipa-otpd/internal.h
new file mode 100644
index 0000000..bec30cb
--- /dev/null
+++ b/daemons/ipa-otpd/internal.h
@@ -0,0 +1,162 @@
+/*
+ * FreeIPA 2FA companion daemon
+ *
+ * Authors: Nathaniel McCallum <npmccallum redhat com>
+ *
+ * Copyright (C) 2013  Nathaniel McCallum, Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef INTERNAL_H_
+#define INTERNAL_H_
+
+#include "k5radius.h"
+
+#include <ldap.h>
+
+#include <errno.h>
+
+#define SECRET ""
+#define log_req(req, ...) req_(__FILE__, __LINE__, (req), __VA_ARGS__)
+#define log_err(errnum, ...) err_(__FILE__, __LINE__, (errnum), __VA_ARGS__)
+
+typedef struct iter_ iter;
+typedef struct item_ item;
+typedef struct queue_ queue;
+
+struct item_ {
+    item *next;
+    k5_radius_packet *req;
+    k5_radius_packet *rsp;
+    size_t sent;
+    char *error;
+
+    struct {
+      char *dn;
+      char *uid;
+      char *ipatokenRadiusUserName;
+      char *ipatokenRadiusConfigLink;
+      char *other;
+    } user;
+
+    struct {
+      char *ipatokenUserMapAttribute;
+      char *ipatokenRadiusSecret;
+      char *ipatokenRadiusServer;
+      time_t ipatokenRadiusTimeout;
+      size_t ipatokenRadiusRetries;
+    } radius;
+    int msgid;
+};
+
+struct queue_ {
+    item *head;
+    item *tail;
+};
+
+struct context {
+  verto_ctx *vctx;
+  krb5_context kctx;
+  k5_radius_client *client;
+  k5_radius_attrset *attrs;
+  int exitstatus;
+
+  struct {
+      verto_ev *reader;
+      verto_ev *writer;
+      queue requests;
+      queue responses;
+  } stdio;
+
+  struct {
+      char *base;
+      verto_ev *io;
+      queue requests;
+      queue responses;
+  } query;
+
+  struct {
+      verto_ev *io;
+      queue requests;
+      queue responses;
+  } bind;
+};
+
+extern struct context ctx;
+
+void
+req_(const char * const file, int line, k5_radius_packet *req,
+     const char * const tmpl, ...);
+
+void
+err_(const char * const file, int line, krb5_error_code code,
+     const char * const tmpl, ...);
+
+krb5_error_code
+item_new(k5_radius_packet *req, item **i);
+
+void
+item_free(item *i);
+
+krb5_error_code
+queue_iter_new(iter **i, ...);
+
+k5_radius_packet *
+queue_iter_func(void *data, krb5_boolean cancel);
+
+void
+queue_push(queue *q, item *i);
+
+void
+queue_push_head(queue *q, item *i);
+
+item *
+queue_peek(queue *q);
+
+item *
+queue_pop(queue *q);
+
+item *
+queue_pop_msgid(queue *q, int msgid);
+
+void
+queue_free_items(queue *q);
+
+void
+on_stdin_readable(verto_ctx *vctx, verto_ev *ev);
+
+void
+on_stdout_writable(verto_ctx *vctx, verto_ev *ev);
+
+void
+on_query_io(verto_ctx *vctx, verto_ev *ev);
+
+void
+on_bind_io(verto_ctx *vctx, verto_ev *ev);
+
+krb5_error_code
+forward(item **i);
+
+const char *
+parse_user(LDAP *ldp, LDAPMessage *entry, item *itm);
+
+const char *
+parse_radius(LDAP *ldp, LDAPMessage *entry, item *itm);
+
+const char *
+parse_radius_username(LDAP *ldp, LDAPMessage *entry, item *itm);
+
+#endif /* INTERNAL_H_ */
diff --git a/daemons/ipa-otpd/ipa-otpd.socket.in b/daemons/ipa-otpd/ipa-otpd.socket.in
new file mode 100644
index 0000000..aefdf3d
--- /dev/null
+++ b/daemons/ipa-otpd/ipa-otpd.socket.in
@@ -0,0 +1,8 @@
+[Unit]
+Description=ipa-otpd socket
+
+[Socket]
+ListenStream= krb5kdcdir@/DEFAULT.socket
+ExecStopPre= UNLINK@ -f @krb5kdcdir@/DEFAULT.socket
+SocketMode=0600
+Accept=true
diff --git a/daemons/ipa-otpd/ipa-otpd  service in b/daemons/ipa-otpd/ipa-otpd  service in
new file mode 100644
index 0000000..b85d5a1
--- /dev/null
+++ b/daemons/ipa-otpd/ipa-otpd  service in
@@ -0,0 +1,9 @@
+[Unit]
+Description=ipa-otpd service
+
+[Service]
+EnvironmentFile= sysconfdir@/ipa/default.conf
+ExecStart= libexecdir@/ipa-otpd $ldap_uri
+StandardInput=socket
+StandardOutput=socket
+StandardError=syslog
diff --git a/daemons/ipa-otpd/main.c b/daemons/ipa-otpd/main.c
new file mode 100644
index 0000000..935ba63
--- /dev/null
+++ b/daemons/ipa-otpd/main.c
@@ -0,0 +1,322 @@
+/*
+ * FreeIPA 2FA companion daemon
+ *
+ * Authors: Nathaniel McCallum <npmccallum redhat com>
+ *
+ * Copyright (C) 2013  Nathaniel McCallum, Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "internal.h"
+
+#include <signal.h>
+#include <stdbool.h>
+
+struct context ctx;
+
+void
+req_(const char * const file, int line, k5_radius_packet *req,
+     const char * const tmpl, ...)
+{
+    const krb5_data *data;
+    va_list ap;
+
+#ifdef DEBUG
+    if (file != NULL)
+        fprintf(stderr, "%8s:%03d: ", file, line);
+#else
+    (void)file;
+    (void)line;
+#endif
+
+    data = k5_radius_packet_get_attr(req, k5_radius_attr_name2num("User-Name"), 0);
+    if (data == NULL)
+        fprintf(stderr, "<unknown>: ");
+    else
+        fprintf(stderr, "%*s: ", data->length, data->data);
+
+    va_start(ap, tmpl);
+    vfprintf(stderr, tmpl, ap);
+    va_end(ap);
+
+    fprintf(stderr, "\n");
+}
+
+void
+err_(const char * const file, int line, krb5_error_code code,
+     const char * const tmpl, ...)
+{
+    const char *msg;
+    va_list ap;
+
+    if (file != NULL)
+        fprintf(stderr, "%10s:%03d: ", file, line);
+
+    if (code != 0) {
+        msg = krb5_get_error_message(ctx.kctx, code);
+        fprintf(stderr, "%s: ", msg);
+        krb5_free_error_message(ctx.kctx, msg);
+    }
+
+    va_start(ap, tmpl);
+    vfprintf(stderr, tmpl, ap);
+    va_end(ap);
+
+    fprintf(stderr, "\n");
+}
+
+static void
+on_ldap_free(verto_ctx *vctx, verto_ev *ev)
+{
+    (void)vctx; /* Unused */
+    ldap_unbind_ext_s(verto_get_private(ev), NULL, NULL);
+}
+
+static void
+on_signal(verto_ctx *vctx, verto_ev *ev)
+{
+    (void)ev; /* Unused */
+    fprintf(stderr, "Signaled, exiting...\n");
+    verto_break(vctx);
+}
+
+char *
+find_base(LDAP *ldp)
+{
+    LDAPMessage *results = NULL, *entry;
+    struct berval **vals = NULL;
+    struct timeval timeout;
+    int i, len;
+    char *tmp = NULL, *attrs[] = {
+        "namingContexts",
+        "defaultNamingContext",
+        NULL
+    };
+
+    timeout.tv_sec = -1;
+    i = ldap_search_ext_s(ldp, "", LDAP_SCOPE_BASE, NULL, attrs,
+                          0, NULL, NULL, &timeout, 1, &results);
+    if (i != LDAP_SUCCESS) {
+        log_err(0, "Unable to search for query base: %s", ldap_err2string(i));
+        goto egress;
+    }
+
+    entry = ldap_first_entry(ldp, results);
+    if (entry == NULL) {
+        log_err(0, "No entries found");
+        goto egress;
+    }
+
+    vals = ldap_get_values_len(ldp, entry, "defaultNamingContext");
+    if (vals == NULL) {
+        vals = ldap_get_values_len(ldp, entry, "namingContexts");
+        if (vals == NULL) {
+            log_err(0, "No namingContexts found");
+            goto egress;
+        }
+    }
+
+    len = ldap_count_values_len(vals);
+    if (len == 1)
+        tmp = strndup(vals[0]->bv_val, vals[0]->bv_len);
+    else
+        log_err(0, "Too many namingContexts found");
+
+    /* TODO: search multiple namingContexts to find the base? */
+
+egress:
+    ldap_value_free_len(vals);
+    ldap_msgfree(results);
+    return tmp;
+}
+
+static krb5_error_code
+setup_ldap(const char *uri, verto_ev **ev, verto_callback *io, char **base)
+{
+    struct timeval timeout;
+    LDAP *ldp;
+    char *tmp;
+    int i;
+
+    i = ldap_initialize(&ldp, uri);
+    if (i != LDAP_SUCCESS)
+        return errno;
+
+    /* Always find the base since this forces open the socket. */
+    tmp = find_base(ldp);
+    if (tmp == NULL)
+        return ENOTCONN;
+    if (base != NULL)
+        *base = tmp;
+    else
+        free(tmp);
+
+    /* Set default timeout to just return immediately for async requests. */
+    memset(&timeout, 0, sizeof(timeout));
+    i = ldap_set_option(ldp, LDAP_OPT_TIMEOUT, &timeout);
+    if (i != LDAP_OPT_SUCCESS) {
+        ldap_unbind_ext_s(ldp, NULL, NULL);
+        return ENOMEM;
+    }
+
+    /* Get the file descriptor. */
+    if (ldap_get_option(ldp, LDAP_OPT_DESC, &i) != LDAP_OPT_SUCCESS) {
+        ldap_unbind_ext_s(ldp, NULL, NULL);
+        return EINVAL;
+    }
+
+    *ev = verto_add_io(ctx.vctx, VERTO_EV_FLAG_PERSIST |
+                                 VERTO_EV_FLAG_IO_ERROR |
+                                 VERTO_EV_FLAG_IO_READ,
+                       io, i);
+    if (*ev == NULL) {
+        ldap_unbind_ext_s(ldp, NULL, NULL);
+        return ENOMEM;
+    }
+
+    verto_set_private(*ev, ldp, on_ldap_free);
+    return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+    char hostname[HOST_NAME_MAX + 1];
+    krb5_error_code retval;
+    krb5_data hndata;
+    verto_ev *tmp;
+
+    if (argc != 2) {
+        fprintf(stderr, "Usage: %s <ldap_uri>\n", argv[0]);
+        return 1;
+    } else {
+        fprintf(stderr, "LDAP: %s\n", argv[1]);
+    }
+
+    memset(&ctx, 0, sizeof(ctx));
+    ctx.exitstatus = 1;
+
+    if (gethostname(hostname, sizeof(hostname)) < 0) {
+        log_err(errno, "Unable to get hostname");
+        goto error;
+    }
+
+    retval = krb5_init_context(&ctx.kctx);
+    if (retval != 0) {
+        log_err(retval, "Unable to initialize context");
+        goto error;
+    }
+
+    ctx.vctx = verto_new(NULL, VERTO_EV_TYPE_IO | VERTO_EV_TYPE_SIGNAL);
+    if (ctx.vctx == NULL) {
+        log_err(ENOMEM, "Unable to initialize event loop");
+        goto error;
+    }
+
+    /* Build attrset. */
+    retval = k5_radius_attrset_new(ctx.kctx, &ctx.attrs);
+    if (retval != 0) {
+        log_err(ENOMEM, "Unable to initialize attrset");
+        goto error;
+    }
+
+    /* Set NAS-Identifier. */
+    hndata.data = hostname;
+    hndata.length = strlen(hndata.data);
+    retval = k5_radius_attrset_add(ctx.attrs,
+                                   k5_radius_attr_name2num("NAS-Identifier"),
+                                   &hndata);
+    if (retval != 0) {
+        log_err(ENOMEM, "Unable to set NAS-Identifier");
+        goto error;
+    }
+
+    /* Set Service-Type. */
+    retval = k5_radius_attrset_add_number(ctx.attrs,
+                                   k5_radius_attr_name2num("Service-Type"),
+                                   K5_RADIUS_SERVICE_TYPE_AUTHENTICATE_ONLY);
+    if (retval != 0) {
+        log_err(ENOMEM, "Unable to set Service-Type");
+        goto error;
+    }
+
+    /* Radius Client */
+    retval = k5_radius_client_new(ctx.kctx, ctx.vctx, &ctx.client);
+    if (retval != 0) {
+        log_err(retval, "Unable to initialize radius client");
+        goto error;
+    }
+
+    /* Signals */
+    tmp = verto_add_signal(ctx.vctx, VERTO_EV_FLAG_NONE, on_signal, SIGTERM);
+    if (tmp == NULL) {
+        log_err(ENOMEM, "Unable to initialize signal event");
+        goto error;
+    }
+    tmp = verto_add_signal(ctx.vctx, VERTO_EV_FLAG_NONE, on_signal, SIGINT);
+    if (tmp == NULL) {
+        log_err(ENOMEM, "Unable to initialize signal event");
+        goto error;
+    }
+
+    /* Standard IO */
+    ctx.stdio.reader = verto_add_io(ctx.vctx, VERTO_EV_FLAG_PERSIST |
+                                              VERTO_EV_FLAG_IO_ERROR |
+                                              VERTO_EV_FLAG_IO_READ,
+                                    on_stdin_readable, STDIN_FILENO);
+    if (ctx.stdio.reader == NULL) {
+        log_err(ENOMEM, "Unable to initialize reader event");
+        goto error;
+    }
+    ctx.stdio.writer = verto_add_io(ctx.vctx, VERTO_EV_FLAG_PERSIST |
+                                              VERTO_EV_FLAG_IO_ERROR |
+                                              VERTO_EV_FLAG_IO_READ,
+                                    on_stdout_writable, STDOUT_FILENO);
+    if (ctx.stdio.writer == NULL) {
+        log_err(ENOMEM, "Unable to initialize writer event");
+        goto error;
+    }
+
+    /* LDAP (Query) */
+    retval = setup_ldap(argv[1], &ctx.query.io, on_query_io, &ctx.query.base);
+    if (retval != 0) {
+        log_err(retval, "Unable to initialize LDAP (Query)");
+        goto error;
+    }
+
+    /* LDAP (Bind) */
+    retval = setup_ldap(argv[1], &ctx.bind.io, on_bind_io, NULL);
+    if (retval != 0) {
+        log_err(retval, "Unable to initialize LDAP (Bind)");
+        goto error;
+    }
+
+    ctx.exitstatus = 0;
+    verto_run(ctx.vctx);
+
+error:
+    k5_radius_client_free(ctx.client);
+    queue_free_items(&ctx.stdio.requests);
+    queue_free_items(&ctx.stdio.responses);
+    queue_free_items(&ctx.query.requests);
+    queue_free_items(&ctx.query.responses);
+    queue_free_items(&ctx.bind.requests);
+    queue_free_items(&ctx.bind.responses);
+    verto_free(ctx.vctx);
+    krb5_free_context(ctx.kctx);
+    return ctx.exitstatus;
+}
+
diff --git a/daemons/ipa-otpd/parse.c b/daemons/ipa-otpd/parse.c
new file mode 100644
index 0000000..2d096d7
--- /dev/null
+++ b/daemons/ipa-otpd/parse.c
@@ -0,0 +1,168 @@
+/*
+ * FreeIPA 2FA companion daemon
+ *
+ * Authors: Nathaniel McCallum <npmccallum redhat com>
+ *
+ * Copyright (C) 2013  Nathaniel McCallum, Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "internal.h"
+#include <ctype.h>
+
+#define DEFAULT_TIMEOUT 15
+#define DEFAULT_RETRIES 3
+
+static int
+get_string(LDAP *ldp, LDAPMessage *entry, const char *name, char **out)
+{
+    struct berval **vals;
+    char *tmp;
+    ber_len_t i;
+
+    vals = ldap_get_values_len(ldp, entry, name);
+    if (vals == NULL)
+        return ENOENT;
+
+    tmp = calloc(vals[0]->bv_len + 1, sizeof(char));
+    if (tmp == NULL) {
+        ldap_value_free_len(vals);
+        return ENOMEM;
+    }
+
+    for (i = 0; i < vals[0]->bv_len; i++) {
+        if (!isprint(vals[0]->bv_val[i])) {
+            free(tmp);
+            ldap_value_free_len(vals);
+            return EINVAL;
+        }
+
+        tmp[i] = vals[0]->bv_val[i];
+    }
+
+    *out = tmp;
+    ldap_value_free_len(vals);
+    return 0;
+}
+
+static int
+get_ulong(LDAP *ldp, LDAPMessage *entry, const char *name, unsigned long *out)
+{
+    unsigned long long i, l;
+    struct berval **vals;
+
+    vals = ldap_get_values_len(ldp, entry, name);
+    if (vals == NULL)
+        return ENOENT;
+
+    for (l = 0, i = 0; i < vals[0]->bv_len; l *= 10, i++) {
+        if (l > ULONG_MAX) {
+            ldap_value_free_len(vals);
+            return ERANGE;
+        }
+
+        if (vals[0]->bv_val[i] < '0' || vals[0]->bv_val[i] > '9') {
+            ldap_value_free_len(vals);
+            return EINVAL;
+        }
+
+        l += vals[0]->bv_val[i] - '0';
+    }
+
+    ldap_value_free_len(vals);
+    *out = l;
+    return 0;
+}
+
+const char *
+parse_user(LDAP *ldp, LDAPMessage *entry, item *itm)
+{
+  int i, j;
+
+  i = get_string(ldp, entry, "uid", &itm->user.uid);
+  if (i != 0)
+      return strerror(i);
+
+  i = get_string(ldp, entry, "ipatokenRadiusUserName",
+                 &itm->user.ipatokenRadiusUserName);
+  if (i != 0 && i != ENOENT)
+      return strerror(i);
+
+  i = get_string(ldp, entry, "ipatokenRadiusConfigLink",
+                 &itm->user.ipatokenRadiusConfigLink);
+  if (i != 0 && i != ENOENT)
+      return strerror(i);
+
+  /* Get the DN. */
+  itm->user.dn = ldap_get_dn(ldp, entry);
+  if (itm->user.dn == NULL) {
+      i = ldap_get_option(ldp, LDAP_OPT_RESULT_CODE, &j);
+      return ldap_err2string(i == LDAP_OPT_SUCCESS ? j : i);
+  }
+
+  return NULL;
+}
+
+const char *
+parse_radius(LDAP *ldp, LDAPMessage *entry, item *itm)
+{
+  unsigned long l;
+  int i;
+
+  i = get_string(ldp, entry, "ipatokenRadiusServer",
+                 &itm->radius.ipatokenRadiusServer);
+  if (i != 0)
+      return strerror(i);
+
+  i = get_string(ldp, entry, "ipatokenRadiusSecret",
+                 &itm->radius.ipatokenRadiusSecret);
+  if (i != 0)
+      return strerror(i);
+
+  i = get_string(ldp, entry, "ipatokenUserMapAttribute",
+                 &itm->radius.ipatokenUserMapAttribute);
+  if (i != 0 && i != ENOENT)
+      return strerror(i);
+
+  i = get_ulong(ldp, entry, "ipatokenRadiusTimeout", &l);
+  if (i == ENOENT)
+      l = DEFAULT_TIMEOUT;
+  else if (i != 0)
+      return strerror(i);
+  itm->radius.ipatokenRadiusTimeout = l * 1000;
+
+  i = get_ulong(ldp, entry, "ipatokenRadiusRetries", &l);
+  if (i == ENOENT)
+      l = DEFAULT_RETRIES;
+  else if (i != 0)
+      return strerror(i);
+  itm->radius.ipatokenRadiusRetries = l;
+
+  return NULL;
+}
+
+const char *
+parse_radius_username(LDAP *ldp, LDAPMessage *entry, item *itm)
+{
+  int i;
+
+  i = get_string(ldp, entry, itm->radius.ipatokenUserMapAttribute,
+                 &itm->user.other);
+  if (i != 0)
+      return strerror(i);
+
+  return NULL;
+}
diff --git a/daemons/ipa-otpd/query.c b/daemons/ipa-otpd/query.c
new file mode 100644
index 0000000..6170aea
--- /dev/null
+++ b/daemons/ipa-otpd/query.c
@@ -0,0 +1,244 @@
+/*
+ * FreeIPA 2FA companion daemon
+ *
+ * Authors: Nathaniel McCallum <npmccallum redhat com>
+ *
+ * Copyright (C) 2013  Nathaniel McCallum, Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define _GNU_SOURCE 1 /* for asprintf() */
+#include "internal.h"
+#include <ctype.h>
+
+#define DEFAULT_TIMEOUT 15
+#define DEFAULT_RETRIES 3
+
+static char *user[] = {
+    "uid",
+    "ipatokenRadiusUserName",
+    "ipatokenRadiusConfigLink",
+    NULL
+};
+
+static char *radius[] = {
+    "ipatokenRadiusServer",
+    "ipatokenRadiusSecret",
+    "ipatokenRadiusTimeout",
+    "ipatokenRadiusRetries",
+    "ipatokenUserMapAttribute",
+    NULL
+};
+
+static void
+on_query_writable(verto_ctx *vctx, verto_ev *ev)
+{
+    queue *push = &ctx.stdio.responses;
+    const krb5_data *princ = NULL;
+    char *filter = NULL, *attrs[2];
+    int i = LDAP_SUCCESS;
+    item *itm;
+    (void)vctx;
+
+    itm = queue_pop(&ctx.query.requests);
+    if (itm == NULL) {
+        verto_set_flags(ctx.query.io, VERTO_EV_FLAG_PERSIST |
+                                      VERTO_EV_FLAG_IO_ERROR |
+                                      VERTO_EV_FLAG_IO_READ);
+        return;
+    }
+
+
+    if (itm->user.dn == NULL) {
+        princ = k5_radius_packet_get_attr(itm->req,
+                                          k5_radius_attr_name2num("User-Name"),
+                                          0);
+        if (princ == NULL)
+            goto error;
+
+        log_req(itm->req, "user query start");
+
+        if (asprintf(&filter, "(&(objectClass=Person)(krbPrincipalName=%*s))",
+                     princ->length, princ->data) < 0)
+            goto error;
+
+        i = ldap_search_ext(verto_get_private(ev), ctx.query.base,
+                            LDAP_SCOPE_SUBTREE, filter, user, 0, NULL,
+                            NULL, NULL, 1, &itm->msgid);
+        free(filter);
+
+    } else if (itm->radius.ipatokenRadiusSecret == NULL) {
+        log_req(itm->req, "radius query start: %s",
+                itm->user.ipatokenRadiusConfigLink);
+
+        i = ldap_search_ext(verto_get_private(ev),
+                            itm->user.ipatokenRadiusConfigLink,
+                            LDAP_SCOPE_BASE, NULL, radius, 0, NULL,
+                            NULL, NULL, 1, &itm->msgid);
+
+    } else if (itm->radius.ipatokenUserMapAttribute != NULL) {
+        log_req(itm->req, "username query start: %s",
+                itm->radius.ipatokenUserMapAttribute);
+
+        attrs[0] = itm->radius.ipatokenUserMapAttribute;
+        attrs[1] = NULL;
+        i = ldap_search_ext(verto_get_private(ev), itm->user.dn,
+                            LDAP_SCOPE_BASE, NULL, attrs, 0, NULL,
+                            NULL, NULL, 1, &itm->msgid);
+    }
+
+    if (i == LDAP_SUCCESS) {
+        itm->sent++;
+        push = &ctx.query.responses;
+    }
+
+error:
+    queue_push(push, itm);
+}
+
+static void
+on_query_readable(verto_ctx *vctx, verto_ev *ev)
+{
+    queue *push = &ctx.stdio.responses;
+    verto_ev *event = ctx.stdio.writer;
+    LDAPMessage *results, *entry;
+    item *itm = NULL;
+    const char *tmp;
+    LDAP *ldp;
+    int i;
+    (void)vctx;
+
+    ldp = verto_get_private(ev);
+
+    i = ldap_result(ldp, LDAP_RES_ANY, 0, NULL, &results);
+    if (i != LDAP_RES_SEARCH_ENTRY && i != LDAP_RES_SEARCH_RESULT) {
+        if (i <= 0)
+            results = NULL;
+        goto egress;
+    }
+
+    itm = queue_pop_msgid(&ctx.query.responses, ldap_msgid(results));
+    if (itm == NULL)
+        goto egress;
+
+    if (i == LDAP_RES_SEARCH_ENTRY) {
+        entry = ldap_first_entry(ldp, results);
+        if (entry == NULL)
+            goto egress;
+
+        tmp = NULL;
+        switch (itm->sent) {
+        case 1:
+            tmp = parse_user(ldp, entry, itm);
+            break;
+        case 2:
+            tmp = parse_radius(ldp, entry, itm);
+            break;
+        case 3:
+            tmp = parse_radius_username(ldp, entry, itm);
+            break;
+        default:
+            goto egress;
+        }
+
+        if (tmp != NULL) {
+            if (itm->error != NULL)
+                free(itm->error);
+            itm->error = strdup(tmp);
+            if (itm->error == NULL)
+               goto egress;
+        }
+
+        queue_push_head(&ctx.query.responses, itm);
+        return;
+    }
+
+    itm->msgid = -1;
+
+    switch (itm->sent) {
+    case 1:
+        log_req(itm->req, "user query end: %s",
+                itm->error == NULL ? itm->user.dn : itm->error);
+        if (itm->user.dn == NULL || itm->user.uid == NULL)
+            goto egress;
+        break;
+    case 2:
+        log_req(itm->req, "radius query end: %s",
+                itm->error == NULL
+                  ? itm->radius.ipatokenRadiusServer
+                  : itm->error);
+        if (itm->radius.ipatokenRadiusServer == NULL ||
+            itm->radius.ipatokenRadiusSecret == NULL)
+            goto egress;
+        break;
+    case 3:
+        log_req(itm->req, "username query end: %s",
+                itm->error == NULL ? itm->user.other : itm->error);
+        break;
+    default:
+        goto egress;
+    }
+
+    if (itm->error != NULL)
+        goto egress;
+
+    if (itm->sent == 1 && itm->user.ipatokenRadiusConfigLink != NULL) {
+        push = &ctx.query.requests;
+        event = ctx.query.io;
+        goto egress;
+    } else if (itm->sent == 2 &&
+               itm->radius.ipatokenUserMapAttribute != NULL &&
+               itm->user.ipatokenRadiusUserName == NULL) {
+        push = &ctx.query.requests;
+        event = ctx.query.io;
+        goto egress;
+    }
+
+    /* Forward to RADIUS if necessary. */
+    i = forward(&itm);
+    if (i != 0)
+        goto egress;
+
+    push = &ctx.bind.requests;
+    event = ctx.bind.io;
+
+egress:
+    ldap_msgfree(results);
+    queue_push(push, itm);
+
+    if (itm != NULL)
+        verto_set_flags(event, VERTO_EV_FLAG_PERSIST |
+                               VERTO_EV_FLAG_IO_ERROR |
+                               VERTO_EV_FLAG_IO_READ |
+                               VERTO_EV_FLAG_IO_WRITE);
+}
+
+void
+on_query_io(verto_ctx *vctx, verto_ev *ev)
+{
+    verto_ev_flag flags;
+
+    flags = verto_get_fd_state(ev);
+    if (flags & VERTO_EV_FLAG_IO_WRITE)
+        on_query_writable(vctx, ev);
+    if (flags & VERTO_EV_FLAG_IO_READ)
+        on_query_readable(vctx, ev);
+    if (flags & VERTO_EV_FLAG_IO_ERROR) {
+        log_err(EIO, "IO error received on query socket");
+        verto_break(ctx.vctx);
+        ctx.exitstatus = 1;
+    }
+}
diff --git a/daemons/ipa-otpd/queue.c b/daemons/ipa-otpd/queue.c
new file mode 100644
index 0000000..3c6c28e
--- /dev/null
+++ b/daemons/ipa-otpd/queue.c
@@ -0,0 +1,207 @@
+/*
+ * FreeIPA 2FA companion daemon
+ *
+ * Authors: Nathaniel McCallum <npmccallum redhat com>
+ *
+ * Copyright (C) 2013  Nathaniel McCallum, Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "internal.h"
+
+struct iter_ {
+    item *next;
+    queue **queues;
+    size_t queue;
+};
+
+krb5_error_code
+item_new(k5_radius_packet *req, item **i)
+{
+    item *tmp;
+
+    /* Push the request to the LDAP Query queue. */
+    tmp = calloc(1, sizeof(item));
+    if (tmp == NULL)
+        return ENOMEM;
+
+    tmp->req = req;
+    tmp->msgid = -1;
+
+    *i = tmp;
+    return 0;
+}
+
+void
+item_free(item *i)
+{
+    if (i == NULL)
+        return;
+
+    ldap_memfree(i->user.dn);
+    free(i->user.uid);
+    free(i->user.ipatokenRadiusUserName);
+    free(i->user.ipatokenRadiusConfigLink);
+    free(i->user.other);
+    free(i->radius.ipatokenRadiusServer);
+    free(i->radius.ipatokenRadiusSecret);
+    free(i->radius.ipatokenUserMapAttribute);
+    free(i->error);
+    k5_radius_packet_free(i->req);
+    k5_radius_packet_free(i->rsp);
+    free(i);
+}
+
+krb5_error_code
+queue_iter_new(iter **i, ...)
+{
+    va_list ap;
+    iter *tmp;
+
+    tmp = calloc(1, sizeof(iter));
+    if (tmp == NULL)
+        return ENOMEM;
+
+    va_start(ap, i);
+    for (tmp->queue = 0; va_arg(ap, queue *) != NULL; tmp->queue++)
+        continue;
+    va_end(ap);
+
+    tmp->queues = calloc(tmp->queue + 1, sizeof(queue *));
+    if (tmp->queues == NULL) {
+        free(tmp);
+        return ENOMEM;
+    }
+
+    va_start(ap, i);
+    for (tmp->queue = 0;
+         (tmp->queues[tmp->queue] = va_arg(ap, queue *)) != NULL;
+         tmp->queue++)
+        continue;
+    va_end(ap);
+
+    tmp->queue = 0;
+    *i = tmp;
+    return 0;
+}
+
+k5_radius_packet *
+queue_iter_func(void *data, krb5_boolean cancel)
+{
+    iter *i = data;
+    item *tmp;
+    queue *q;
+
+    if (cancel) {
+        free(i->queues);
+        free(i);
+        return NULL;
+    }
+
+    if (i->next != NULL) {
+        tmp = i->next;
+        i->next = tmp->next;
+        return tmp->req;
+    }
+
+    q = i->queues[i->queue++];
+    if (q == NULL)
+        return queue_iter_func(data, TRUE);
+
+    i->next = q->head;
+    return queue_iter_func(data, FALSE);
+}
+
+void
+queue_push(queue *q, item *i)
+{
+    if (i == NULL)
+        return;
+
+    if (q->tail == NULL)
+        q->head = q->tail = i;
+    else
+        q->tail = q->tail->next = i;
+}
+
+void
+queue_push_head(queue *q, item *i)
+{
+    if (i == NULL)
+        return;
+
+    if (q->head == NULL)
+        q->tail = q->head = i;
+    else {
+        i->next = q->head;
+        q->head = i;
+    }
+}
+
+item *
+queue_peek(queue *q)
+{
+    return q->head;
+}
+
+item *
+queue_pop(queue *q)
+{
+    item *tmp;
+
+    if (q == NULL)
+        return NULL;
+
+    tmp = q->head;
+    if (tmp != NULL)
+        q->head = tmp->next;
+
+    if (q->head == NULL)
+        q->tail = NULL;
+
+    return tmp;
+}
+
+item *
+queue_pop_msgid(queue *q, int msgid)
+{
+    item *tmp, **prev;
+
+    for (tmp = q->head, prev = &q->head;
+         tmp != NULL;
+         tmp = tmp->next, prev = &tmp->next) {
+        if (tmp->msgid == msgid) {
+            *prev = tmp->next;
+            if (q->head == NULL)
+                q->tail = NULL;
+            return tmp;
+        }
+    }
+
+    return NULL;
+}
+
+void
+queue_free_items(queue *q)
+{
+    item *tmp;
+
+    for (tmp = q->head; tmp != NULL; tmp = tmp->next)
+        item_free(tmp);
+
+    q->head = NULL;
+    q->tail = NULL;
+}
diff --git a/daemons/ipa-otpd/stdio.c b/daemons/ipa-otpd/stdio.c
new file mode 100644
index 0000000..d490690
--- /dev/null
+++ b/daemons/ipa-otpd/stdio.c
@@ -0,0 +1,190 @@
+/*
+ * FreeIPA 2FA companion daemon
+ *
+ * Authors: Nathaniel McCallum <npmccallum redhat com>
+ *
+ * Copyright (C) 2013  Nathaniel McCallum, Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "internal.h"
+
+void
+on_stdin_readable(verto_ctx *vctx, verto_ev *ev)
+{
+    static char _buffer[K5_RADIUS_PACKET_SIZE_MAX];
+    static krb5_data buffer = { .data = _buffer };
+    (void)vctx;
+
+    const krb5_data *data;
+    k5_radius_packet *req;
+    ssize_t pktlen;
+    iter *itr;
+    item *tmp;
+    int i;
+
+    pktlen = k5_radius_packet_bytes_needed(&buffer);
+    if (pktlen < 0) {
+        log_err(EBADMSG, "Recevied a malformed item");
+        goto shutdown;
+    }
+
+    /* Read the item. */
+    i = read(verto_get_fd(ev), buffer.data + buffer.length, pktlen);
+    if (i < 1) {
+        /* On EOF, shutdown gracefully. */
+        if (i == 0) {
+            fprintf(stderr, "Socket closed, shutting down...\n");
+            verto_break(ctx.vctx);
+            return;
+        }
+
+        if (errno != EAGAIN && errno != EINTR) {
+            log_err(errno, "Error receiving packet");
+            goto shutdown;
+        }
+
+        return;
+    }
+
+    /* If we have a partial read or just the header, try again. */
+    buffer.length += i;
+    pktlen = k5_radius_packet_bytes_needed(&buffer);
+    if (pktlen > 0)
+        return;
+
+    /* Create the iterator. */
+    i = queue_iter_new(&itr,
+                       &ctx.stdio.requests,
+                       &ctx.stdio.responses,
+                       &ctx.query.requests,
+                       &ctx.query.responses,
+                       &ctx.bind.requests,
+                       &ctx.bind.responses, NULL);
+    if (i != 0) {
+        log_err(i, "Unable to create iterator");
+        goto shutdown;
+    }
+
+    /* Decode the item. */
+    i = k5_radius_packet_decode(ctx.kctx, SECRET, &buffer, TRUE,
+                                queue_iter_func, itr, &req, NULL);
+    buffer.length = 0;
+    if (i == EAGAIN)
+        return;
+    else if (i != 0) {
+        log_err(i, "Unable to decode item");
+        goto shutdown;
+    }
+
+    /* Ensure the packet has the User-Name attribute. */
+    data = k5_radius_packet_get_attr(req,
+                                     k5_radius_attr_name2num("User-Name"),
+                                     0);
+    if (data == NULL) {
+        k5_radius_packet_free(req);
+        return;
+    }
+
+    /* Create the new queue item. */
+    i = item_new(req, &tmp);
+    if (i != 0) {
+        k5_radius_packet_free(req);
+        return;
+    }
+
+    /* Push it to the query queue. */
+    queue_push(&ctx.query.requests, tmp);
+    verto_set_flags(ctx.query.io, VERTO_EV_FLAG_PERSIST |
+                                  VERTO_EV_FLAG_IO_ERROR |
+                                  VERTO_EV_FLAG_IO_READ |
+                                  VERTO_EV_FLAG_IO_WRITE);
+
+    log_req(req, "request received");
+    return;
+
+shutdown:
+    verto_break(ctx.vctx);
+    ctx.exitstatus = 1;
+}
+
+void
+on_stdout_writable(verto_ctx *vctx, verto_ev *ev)
+{
+    const krb5_data *data;
+    item *itm;
+    int i;
+    (void)vctx;
+
+    itm = queue_peek(&ctx.stdio.responses);
+    if (itm == NULL) {
+        verto_set_flags(ctx.stdio.writer, VERTO_EV_FLAG_PERSIST |
+                                    VERTO_EV_FLAG_IO_ERROR |
+                                    VERTO_EV_FLAG_IO_READ);
+        return;
+    }
+
+    /* If no response has been generated thus far, send Access-Reject. */
+    if (itm->rsp == NULL) {
+        itm->sent = 0;
+        i = k5_radius_packet_new_response(ctx.kctx, SECRET,
+                                      k5_radius_code_name2num("Access-Reject"),
+                                      NULL, itm->req, &itm->rsp);
+        if (i != 0) {
+            log_err(errno, "Unable to craft response");
+            goto shutdown;
+        }
+    }
+
+    /* Send the packet. */
+    data = k5_radius_packet_encode(itm->rsp);
+    i = write(verto_get_fd(ev), data->data + itm->sent,
+              data->length - itm->sent);
+    if (i < 0) {
+        switch (errno) {
+#if defined(EWOULDBLOCK) && (!defined(EAGAIN) || EAGAIN - EWOULDBLOCK != 0)
+        case EWOULDBLOCK:
+#endif
+#if defined(EAGAIN)
+        case EAGAIN:
+#endif
+        case ENOBUFS:
+        case EINTR:
+            /* In this case, we just need to try again. */
+            return;
+        default:
+            /* Unrecoverable. */
+            break;
+        }
+
+        log_err(errno, "Error writing to stdout!");
+        goto shutdown;
+    }
+
+    /* If the packet was completely sent, free the response. */
+    itm->sent += i;
+    if (itm->sent == data->length) {
+        log_req(itm->req, "response sent: %s",
+                k5_radius_code_num2name(k5_radius_packet_get_code(itm->rsp)));
+        item_free(queue_pop(&ctx.stdio.responses));
+    }
+
+    return;
+
+shutdown:
+    verto_break(ctx.vctx);
+    ctx.exitstatus = 1;
+}
diff --git a/daemons/ipa-otpd/test.py b/daemons/ipa-otpd/test.py
new file mode 100644
index 0000000..b8afead
--- /dev/null
+++ b/daemons/ipa-otpd/test.py
@@ -0,0 +1,61 @@
+#!/usr/bin/python
+#
+# FreeIPA 2FA companion daemon
+#
+# Authors: Nathaniel McCallum <npmccallum redhat com>
+#
+# Copyright (C) 2013  Nathaniel McCallum, Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import StringIO
+import struct
+import subprocess
+import sys
+
+try:
+    from pyrad import packet
+    from pyrad.dictionary import Dictionary
+except ImportError:
+    sys.stdout.write("pyrad not found!\n")
+    sys.exit(0)
+
+# We could use a dictionary file, but since we need
+# such few attributes, we'll just include them here
+DICTIONARY = """
+ATTRIBUTE	User-Name	1	string
+ATTRIBUTE	User-Password	2	string
+ATTRIBUTE	NAS-Identifier	32	string
+"""
+
+dct = Dictionary(StringIO.StringIO(DICTIONARY))
+
+proc = subprocess.Popen(["./ipa-otpd", sys.argv[1]],
+                        stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+
+pkt = packet.AuthPacket(secret="", dict=dct)
+pkt["User-Name"] = sys.argv[2]
+pkt["User-Password"] = pkt.PwCrypt(sys.argv[3])
+pkt["NAS-Identifier"] = "localhost"
+proc.stdin.write(pkt.RequestPacket())
+
+rsp = packet.Packet(secret="", dict=dict)
+buf = proc.stdout.read(4)
+buf += proc.stdout.read(struct.unpack("!BBH", buf)[2] - 4)
+rsp.DecodePacket(buf)
+pkt.VerifyReply(rsp)
+
+proc.terminate()
+proc.wait()
diff --git a/install/share/70ipaotp.ldif b/install/share/70ipaotp.ldif
new file mode 100644
index 0000000..0b8b4c0
--- /dev/null
+++ b/install/share/70ipaotp.ldif
@@ -0,0 +1,159 @@
+# FreeIPA tokens schema
+# BaseOID: 2.16.840.1.113730.3.8.16
+# We use ipatoken as "namespace"
+# See RFC 4517 for Syntax OID definitions
+dn: cn=schema
+#
+# Token related attributes
+#
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.1 NAME 'ipatokenUniqueID'
+                    DESC 'Token Unique Identifier'
+                    EQUALITY caseIgnoreMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.2 NAME 'ipatokenDisabled'
+                    DESC 'Optional, marks token as Disabled'
+                    EQUALITY booleanMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.3 NAME 'ipatokenNotBefore'
+                    DESC 'Token validity date'
+                    EQUALITY generalizedTimeMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.4 NAME 'ipatokenNotAfter'
+                    DESC 'Token expiration date'
+                    EQUALITY generalizedTimeMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.5 NAME 'ipatokenVendor'
+                    DESC 'Optional Vendor identifier'
+                    EQUALITY caseIgnoreMatch
+                    SUBSTR caseIgnoreSubstringsMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.6 NAME 'ipatokenModel'
+                    DESC 'Optional Model identifier'
+                    EQUALITY caseIgnoreMatch
+                    SUBSTR caseIgnoreSubstringsMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.7 NAME 'ipatokenSerial'
+                    DESC 'OTP Token Serial number'
+                    EQUALITY caseIgnoreMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.8 NAME 'ipatokenOTPkey'
+                    DESC 'OTP Token Key'
+                    EQUALITY octetStringMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.40
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.9 NAME 'ipatokenOTPalgorithm'
+                    DESC 'OTP Token Algorithm'
+                    EQUALITY caseIgnoreMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.10 NAME 'ipatokenOTPdigits'
+                    DESC 'OTP Token Number of digits'
+                    EQUALITY integerMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.11 NAME 'ipatokenOTPclockOffset'
+                    DESC 'OTP Token clock offset'
+                    EQUALITY integerMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.12 NAME 'ipatokenTOTPtimeStep'
+                    DESC 'TOTP time-step'
+                    EQUALITY integerMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.13 NAME 'ipatokenOwner'
+                    DESC 'User entry that owns this token'
+                    SUP distinguishedName
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+#
+# Token related objectclasses
+#
+objectclasses: ( 2.16.840.1.113730.3.8.16.2.1 NAME 'ipaToken' SUP top ABSTRACT
+                    DESC 'Abstract token class for tokens'
+                    MUST ( ipatokenUniqueID )
+                    MAY ( description $ ipatokenOwner $ ipatokenDisabled $ ipatokenNotBefore $
+                              ipatokenNotAfter $  ipatokenVendor $ ipatokenModel $ ipatokenSerial)
+                    X-ORIGIN 'FreeIPA' )
+                    
+objectclasses: ( 2.16.840.1.113730.3.8.16.2.2 NAME 'ipatokenTOTP' SUP ipatokenOTP STRUCTURAL
+                    DESC 'TOTP Token Type'
+                    MAY( ipatokenOTPkey $ ipatokenOTPalgorithm $ ipatokenOTPdigits $
+                             ipatokenOTPclockOffset $ ipatokenTOTPtimeStep)
+                    X-ORIGIN 'FreeIPA' )
+#
+# RADIUS user related attributes and objectclasses
+#
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.14 NAME 'ipatokenRadiusUserName'
+                    DESC 'Corresponding Radius username'
+                    EQUALITY caseIgnoreMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.15 NAME 'ipatokenRadiusConfigLink'
+                    DESC 'Corresponding Radius Configuration link'
+                    SUP distinguishedName
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+objectclasses: ( 2.16.840.1.113730.3.8.16.2.4 NAME 'ipatokenRadiusProxyUser' SUP top AUXILIARY
+                    DESC 'Radius Proxy User'
+                    MUST ( ipatokenRadiusConfigLink)
+                    MAY( ipatokenRadiusUserName )
+                    X-ORIGIN 'FreeIPA' )
+#
+#Radius related attributes and Objectclasses
+#
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.16 NAME 'ipatokenRadiusServer'
+                    DESC 'Server String Configuration'
+                    EQUALITY caseIgnoreIA5Match
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.17 NAME 'ipatokenRadiusSecret'
+                    DESC 'Server's Secret'
+                    EQUALITY octetStringMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.40
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.18 NAME 'ipatokenRadiusTimeout'
+                    DESC 'Server Timeout'
+                    EQUALITY integerMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.19 NAME 'ipatokenRadiusRetries'
+                    DESC 'Number of allowed Retries'
+                    EQUALITY integerMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+attributetypes: ( 2.16.840.1.113730.3.8.16.1.20 NAME 'ipatokenUserMapAttribute'
+                    DESC 'Attribute to map from the user entry for RADIUS server authentication'
+                    EQUALITY caseIgnoreMatch
+                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+                    SINGLE-VALUE
+                    X-ORIGIN 'FreeIPA' )
+objectclasses: ( 2.16.840.1.113730.3.8.16.2.10 NAME 'ipatokenRadiusConfiguration' SUP top STRUCTURAL
+                   DESC 'Proxy Radius Configuration'
+                   MUST ( cn $ ipatokenRadiusServer $ ipatokenRadiusSecret )
+                   MAY ( description $ ipatokenRadiusTimeout $ ipatokenRadiusRetries $
+                             ipatokenUserMapAttribute )
+                   X-ORIGIN 'FreeIPA' )
diff --git a/install/share/Makefile.am b/install/share/Makefile.am
index f8f9b74..8823723 100644
--- a/install/share/Makefile.am
+++ b/install/share/Makefile.am
@@ -11,6 +11,7 @@ app_DATA =				\
 	60ipadns.ldif			\
 	61kerberos-ipav3.ldif		\
 	65ipasudo.ldif			\
+	70ipaotp.ldif			\
 	anonymous-vlv.ldif		\
 	bootstrap-template.ldif		\
 	caJarSigningCert.cfg.template	\
diff --git a/install/share/copy-schema-to-ca.py b/install/share/copy-schema-to-ca.py
index 4e2054e..1888f12 100755
--- a/install/share/copy-schema-to-ca.py
+++ b/install/share/copy-schema-to-ca.py
@@ -31,6 +31,7 @@ SCHEMA_FILENAMES = (
     "60ipadns.ldif",
     "61kerberos-ipav3.ldif",
     "65ipasudo.ldif",
+    "70ipaotp.ldif",
     "05rfc2247.ldif",
 )
 
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
index 77d76a6..02de66e 100644
--- a/ipaserver/install/dsinstance.py
+++ b/ipaserver/install/dsinstance.py
@@ -412,7 +412,8 @@ class DsInstance(service.Service):
                              "60basev3.ldif",
                              "60ipadns.ldif",
                              "61kerberos-ipav3.ldif",
-                             "65ipasudo.ldif"):
+                             "65ipasudo.ldif",
+                             "70ipaotp.ldif"):
             target_fname = schema_dirname(self.serverid) + schema_fname
             shutil.copyfile(ipautil.SHARE_DIR + schema_fname, target_fname)
             os.chmod(target_fname, 0440)    # read access for dirsrv user/group
-- 
1.8.1.4


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]