From 203754691c28243dd3cf378e98390fc0a455b485 Mon Sep 17 00:00:00 2001 From: Nathaniel McCallum Date: Thu, 11 Apr 2013 14:03:25 -0400 Subject: Add the krb5/FreeIPA RADIUS companion daemon This daemon listens for RADIUS packets on a well known UNIX domain socket. When a packet is received, it queries LDAP to see if the user is configured for RADIUS authentication. If so, then the packet is forwarded to the 3rd party RADIUS server. Otherwise, a bind is attempted against the LDAP server. https://fedorahosted.org/freeipa/ticket/3366 http://freeipa.org/page/V3/OTP --- daemons/Makefile.am | 1 + daemons/configure.ac | 97 ++++------ daemons/ipa-otpd/Makefile.am | 21 +++ daemons/ipa-otpd/bind.c | 144 ++++++++++++++ daemons/ipa-otpd/forward.c | 124 +++++++++++++ daemons/ipa-otpd/internal.h | 153 +++++++++++++++ daemons/ipa-otpd/ipa-otpd.socket.in | 11 ++ daemons/ipa-otpd/ipa-otpd@.service.in | 9 + daemons/ipa-otpd/main.c | 340 ++++++++++++++++++++++++++++++++++ daemons/ipa-otpd/parse.c | 176 ++++++++++++++++++ daemons/ipa-otpd/query.c | 253 +++++++++++++++++++++++++ daemons/ipa-otpd/queue.c | 183 ++++++++++++++++++ daemons/ipa-otpd/stdio.c | 205 ++++++++++++++++++++ daemons/ipa-otpd/test.py | 61 ++++++ 14 files changed, 1718 insertions(+), 60 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 (limited to 'daemons') diff --git a/daemons/Makefile.am b/daemons/Makefile.am index 05cd1a76..956f399b 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 3e8e81f5..371c28d0 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(krad.h, [], [AC_MSG_ERROR([krad.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(krad, main, [], [AC_MSG_ERROR([libkrad not found])]) +KRB5_LIBS="-lkrb5 -l$krb5crypto -lcom_err" +KRAD_LIBS="-lkrad" +krb5kdcdir="${localstatedir}/kerberos/krb5kdc" AC_SUBST(KRB5_LIBS) +AC_SUBST(KRAD_LIBS) +AC_SUBST(krb5kdcdir) dnl --------------------------------------------------------------------------- dnl - Check for Mozilla LDAP and OpenLDAP SDK @@ -262,6 +216,11 @@ AC_CHECK_LIB([pdb],[pdb_enum_upn_suffixes], [AC_MSG_WARN([libpdb does not have pdb_enum_upn_suffixes, no support for realm domains in ipasam])], [$SAMBA40EXTRA_LIBPATH]) +dnl --------------------------------------------------------------------------- +dnl Check for libverto +dnl --------------------------------------------------------------------------- +PKG_CHECK_MODULES([LIBVERTO], [libverto]) + dnl --------------------------------------------------------------------------- dnl - Check for check unit test framework http://check.sourceforge.net/ dnl --------------------------------------------------------------------------- @@ -309,6 +268,20 @@ PKG_CHECK_MODULES([DIRSRV], [dirsrv >= 1.3.0]) 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 --------------------------------------------------------------------------- @@ -373,6 +346,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-dns/Makefile @@ -394,19 +368,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} + KRAD libs: ${KRAD_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 00000000..ed99c3ec --- /dev/null +++ b/daemons/ipa-otpd/Makefile.am @@ -0,0 +1,21 @@ +AM_CFLAGS := $(CFLAGS) @LDAP_CFLAGS@ @LIBVERTO_CFLAGS@ +AM_LDFLAGS := $(LDFLAGS) @LDAP_LIBS@ @LIBVERTO_LIBS@ @KRAD_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 00000000..c985ccd7 --- /dev/null +++ b/daemons/ipa-otpd/bind.c @@ -0,0 +1,144 @@ +/* + * FreeIPA 2FA companion daemon + * + * Authors: Nathaniel McCallum + * + * 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 . + */ + +/* + * This file takes requests from query.c and performs an LDAP bind on behalf + * of the user. The results are placed in the stdout queue (stdio.c). + */ + +#include "internal.h" + +static void on_bind_writable(verto_ctx *vctx, verto_ev *ev) +{ + struct otpd_queue *push = &ctx.stdio.responses; + const krb5_data *data; + struct berval cred; + struct otpd_queue_item *item; + int i; + (void)vctx; + + item = otpd_queue_pop(&ctx.bind.requests); + if (item == NULL) { + verto_set_flags(ctx.bind.io, VERTO_EV_FLAG_PERSIST | + VERTO_EV_FLAG_IO_ERROR | + VERTO_EV_FLAG_IO_READ); + return; + } + + if (item->user.dn == NULL) + goto error; + + data = krad_packet_get_attr(item->req, + krad_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), item->user.dn, LDAP_SASL_SIMPLE, + &cred, NULL, NULL, &item->msgid); + if (i != LDAP_SUCCESS) { + otpd_log_err(errno, "Unable to initiate bind: %s", ldap_err2string(i)); + verto_break(ctx.vctx); + ctx.exitstatus = 1; + } + + otpd_log_req(item->req, "bind start: %s", item->user.dn); + push = &ctx.bind.responses; + +error: + otpd_queue_push(push, item); +} + +static void on_bind_readable(verto_ctx *vctx, verto_ev *ev) +{ + const char *errstr = "error"; + LDAPMessage *results; + struct otpd_queue_item *item = 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; + } + + item = otpd_queue_pop_msgid(&ctx.bind.responses, ldap_msgid(results)); + if (item == NULL) { + ldap_msgfree(results); + return; + } + item->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; + } + + item->sent = 0; + i = krad_packet_new_response(ctx.kctx, SECRET, + krad_code_name2num("Access-Accept"), + NULL, item->req, &item->rsp); + if (i != 0) { + errstr = krb5_get_error_message(ctx.kctx, i); + goto error; + } + +error: + if (item != NULL) + otpd_log_req(item->req, "bind end: %s", + item->rsp != NULL ? "success" : errstr); + + ldap_msgfree(results); + otpd_queue_push(&ctx.stdio.responses, item); + 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 otpd_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) { + otpd_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 00000000..e6ae1e9d --- /dev/null +++ b/daemons/ipa-otpd/forward.c @@ -0,0 +1,124 @@ +/* + * FreeIPA 2FA companion daemon + * + * Authors: Nathaniel McCallum + * + * 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 . + */ + +/* + * This file proxies the incoming RADIUS request (stdio.c/query.c) to a + * third-party RADIUS server if the user is configured for forwarding. The + * result is placed in the stdout queue (stdio.c). + */ + +#include "internal.h" + +static void forward_cb(krb5_error_code retval, const krad_packet *request, + const krad_packet *response, void *data) +{ + krad_code code, acpt; + struct otpd_queue_item *item = data; + (void)request; + + acpt = krad_code_name2num("Access-Accept"); + code = krad_packet_get_code(response); + if (retval == 0 && code == acpt) { + item->sent = 0; + retval = krad_packet_new_response(ctx.kctx, SECRET, acpt, + NULL, item->req, &item->rsp); + } + + otpd_log_req(item->req, "forward end: %s", + retval == 0 + ? krad_code_num2name(code) + : krb5_get_error_message(ctx.kctx, retval)); + + otpd_queue_push(&ctx.stdio.responses, item); + 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 otpd_forward(struct otpd_queue_item **item) +{ + krad_attr usernameid, passwordid; + const krb5_data *password; + krb5_error_code retval; + char *username; + krb5_data data; + + /* Find the username. */ + username = (*item)->user.ipatokenRadiusUserName; + if (username == NULL) { + username = (*item)->user.other; + if (username == NULL) + username = (*item)->user.uid; + } + + /* Check to see if we are supposed to forward. */ + if ((*item)->radius.ipatokenRadiusServer == NULL || + (*item)->radius.ipatokenRadiusSecret == NULL || + username == NULL) + return 0; + + otpd_log_req((*item)->req, "forward start: %s / %s", username, + (*item)->radius.ipatokenRadiusServer); + + usernameid = krad_attr_name2num("User-Name"); + passwordid = krad_attr_name2num("User-Password"); + + /* Set User-Name. */ + data.data = username; + data.length = strlen(data.data); + retval = krad_attrset_add(ctx.attrs, usernameid, &data); + if (retval != 0) + goto error; + + /* Set User-Password. */ + password = krad_packet_get_attr((*item)->req, passwordid, 0); + if (password == NULL) { + krad_attrset_del(ctx.attrs, usernameid, 0); + goto error; + } + retval = krad_attrset_add(ctx.attrs, passwordid, password); + if (retval != 0) { + krad_attrset_del(ctx.attrs, usernameid, 0); + goto error; + } + + /* Forward the request to the RADIUS server. */ + retval = krad_client_send(ctx.client, + krad_code_name2num("Access-Request"), + ctx.attrs, + (*item)->radius.ipatokenRadiusServer, + (*item)->radius.ipatokenRadiusSecret, + (*item)->radius.ipatokenRadiusTimeout, + (*item)->radius.ipatokenRadiusRetries, + forward_cb, *item); + krad_attrset_del(ctx.attrs, usernameid, 0); + krad_attrset_del(ctx.attrs, passwordid, 0); + if (retval == 0) + *item = NULL; + +error: + if (retval != 0) + otpd_log_req((*item)->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 00000000..5ab4a777 --- /dev/null +++ b/daemons/ipa-otpd/internal.h @@ -0,0 +1,153 @@ +/* + * FreeIPA 2FA companion daemon + * + * Authors: Nathaniel McCallum + * + * 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 . + */ + +#ifndef INTERNAL_H_ +#define INTERNAL_H_ + +#include "krad.h" + +#include + +#include + +#define SECRET "" +#define otpd_log_req(req, ...) \ + otpd_log_req_(__FILE__, __LINE__, (req), __VA_ARGS__) +#define otpd_log_err(errnum, ...) \ + otpd_log_err_(__FILE__, __LINE__, (errnum), __VA_ARGS__) + +struct otpd_queue_iter; + +struct otpd_queue_item { + struct otpd_queue_item *next; + krad_packet *req; + krad_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 otpd_queue { + struct otpd_queue_item *head; + struct otpd_queue_item *tail; +}; + +/* This structure contains our global state. The most important part is the + * queues. When a request comes in (stdio.c), it is placed into an item object. + * This item exists in only one queue at a time as it flows through this + * daemon. + * + * The flow is: stdin => query => (forward (no queue) or bind) => stdout. + */ +struct otpd_context { + verto_ctx *vctx; + krb5_context kctx; + krad_client *client; + krad_attrset *attrs; + int exitstatus; + + struct { + verto_ev *reader; + verto_ev *writer; + struct otpd_queue responses; + } stdio; + + struct { + char *base; + verto_ev *io; + struct otpd_queue requests; + struct otpd_queue responses; + } query; + + struct { + verto_ev *io; + struct otpd_queue requests; + struct otpd_queue responses; + } bind; +}; + +extern struct otpd_context ctx; + +void otpd_log_req_(const char * const file, int line, krad_packet *req, + const char * const tmpl, ...); + +void otpd_log_err_(const char * const file, int line, krb5_error_code code, + const char * const tmpl, ...); + +krb5_error_code otpd_queue_item_new(krad_packet *req, + struct otpd_queue_item **item); + +void otpd_queue_item_free(struct otpd_queue_item *item); + +krb5_error_code otpd_queue_iter_new(const struct otpd_queue * const *queues, + struct otpd_queue_iter **iter); + +const krad_packet *otpd_queue_iter_func(void *data, krb5_boolean cancel); + +void otpd_queue_push(struct otpd_queue *q, struct otpd_queue_item *item); + +void otpd_queue_push_head(struct otpd_queue *q, struct otpd_queue_item *item); + +struct otpd_queue_item *otpd_queue_peek(struct otpd_queue *q); + +struct otpd_queue_item *otpd_queue_pop(struct otpd_queue *q); + +struct otpd_queue_item *otpd_queue_pop_msgid(struct otpd_queue *q, int msgid); + +void otpd_queue_free_items(struct otpd_queue *q); + +void otpd_on_stdin_readable(verto_ctx *vctx, verto_ev *ev); + +void otpd_on_stdout_writable(verto_ctx *vctx, verto_ev *ev); + +void otpd_on_query_io(verto_ctx *vctx, verto_ev *ev); + +void otpd_on_bind_io(verto_ctx *vctx, verto_ev *ev); + +krb5_error_code otpd_forward(struct otpd_queue_item **i); + +const char *otpd_parse_user(LDAP *ldp, LDAPMessage *entry, + struct otpd_queue_item *item); + +const char *otpd_parse_radius(LDAP *ldp, LDAPMessage *entry, + struct otpd_queue_item *item); + +const char *otpd_parse_radius_username(LDAP *ldp, LDAPMessage *entry, + struct otpd_queue_item *item); + +#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 00000000..b968beaa --- /dev/null +++ b/daemons/ipa-otpd/ipa-otpd.socket.in @@ -0,0 +1,11 @@ +[Unit] +Description=ipa-otpd socket + +[Socket] +ListenStream=@krb5kdcdir@/DEFAULT.socket +ExecStopPre=@UNLINK@ @krb5kdcdir@/DEFAULT.socket +SocketMode=0600 +Accept=true + +[Install] +WantedBy=krb5kdc.service diff --git a/daemons/ipa-otpd/ipa-otpd@.service.in b/daemons/ipa-otpd/ipa-otpd@.service.in new file mode 100644 index 00000000..b85d5a12 --- /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 00000000..a5d1f93f --- /dev/null +++ b/daemons/ipa-otpd/main.c @@ -0,0 +1,340 @@ +/* + * FreeIPA 2FA companion daemon + * + * Authors: Nathaniel McCallum + * + * 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 . + */ + +/* + * This file initializes a systemd socket-activated daemon which receives + * RADIUS packets on STDIN and either proxies them to a third party RADIUS + * server or performs authentication directly by binding to the LDAP server. + * The choice between bind or proxy is made by evaluating LDAP configuration + * for the given user. + */ + +#include "internal.h" + +#include +#include + +/* Our global state. */ +struct otpd_context ctx; + +/* Implementation function for logging a request's state. See internal.h. */ +void otpd_log_req_(const char * const file, int line, krad_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 = krad_packet_get_attr(req, krad_attr_name2num("User-Name"), 0); + if (data == NULL) + fprintf(stderr, ": "); + else + fprintf(stderr, "%*s: ", data->length, data->data); + + va_start(ap, tmpl); + vfprintf(stderr, tmpl, ap); + va_end(ap); + + fprintf(stderr, "\n"); +} + +/* Implementation function for logging a generic error. See internal.h. */ +void otpd_log_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); +} + +static char *find_base(LDAP *ldp) +{ + LDAPMessage *results = NULL, *entry; + struct berval **vals = NULL; + struct timeval timeout; + int i, len; + char *base = 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) { + otpd_log_err(0, "Unable to search for query base: %s", + ldap_err2string(i)); + goto egress; + } + + entry = ldap_first_entry(ldp, results); + if (entry == NULL) { + otpd_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) { + otpd_log_err(0, "No namingContexts found"); + goto egress; + } + } + + len = ldap_count_values_len(vals); + if (len == 1) + base = strndup(vals[0]->bv_val, vals[0]->bv_len); + else + otpd_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 base; +} + +/* Set up an LDAP connection as a verto event. */ +static krb5_error_code setup_ldap(const char *uri, krb5_boolean bind, + verto_callback *io, verto_ev **ev, + char **base) +{ + struct timeval timeout; + int err, ver, fd; + char *basetmp; + LDAP *ldp; + + err = ldap_initialize(&ldp, uri); + if (err != LDAP_SUCCESS) + return errno; + + ver = LDAP_VERSION3; + ldap_set_option(ldp, LDAP_OPT_PROTOCOL_VERSION, &ver); + + if (bind) { + err = ldap_sasl_bind_s(ldp, NULL, "EXTERNAL", NULL, NULL, NULL, NULL); + if (err != LDAP_SUCCESS) + return errno; + } + + /* Always find the base since this forces open the socket. */ + basetmp = find_base(ldp); + if (basetmp == NULL) + return ENOTCONN; + if (base != NULL) + *base = basetmp; + else + free(basetmp); + + /* Set default timeout to just return immediately for async requests. */ + memset(&timeout, 0, sizeof(timeout)); + err = ldap_set_option(ldp, LDAP_OPT_TIMEOUT, &timeout); + if (err != LDAP_OPT_SUCCESS) { + ldap_unbind_ext_s(ldp, NULL, NULL); + return ENOMEM; /* What error code do I use? */ + } + + /* Get the file descriptor. */ + if (ldap_get_option(ldp, LDAP_OPT_DESC, &fd) != 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, fd); + if (*ev == NULL) { + ldap_unbind_ext_s(ldp, NULL, NULL); + return ENOMEM; /* What error code do I use? */ + } + + 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 *sig; + + if (argc != 2) { + fprintf(stderr, "Usage: %s \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) { + otpd_log_err(errno, "Unable to get hostname"); + goto error; + } + + retval = krb5_init_context(&ctx.kctx); + if (retval != 0) { + otpd_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) { + otpd_log_err(ENOMEM, "Unable to initialize event loop"); + goto error; + } + + /* Build attrset. */ + retval = krad_attrset_new(ctx.kctx, &ctx.attrs); + if (retval != 0) { + otpd_log_err(retval, "Unable to initialize attrset"); + goto error; + } + + /* Set NAS-Identifier. */ + hndata.data = hostname; + hndata.length = strlen(hndata.data); + retval = krad_attrset_add(ctx.attrs, krad_attr_name2num("NAS-Identifier"), + &hndata); + if (retval != 0) { + otpd_log_err(retval, "Unable to set NAS-Identifier"); + goto error; + } + + /* Set Service-Type. */ + retval = krad_attrset_add_number(ctx.attrs, + krad_attr_name2num("Service-Type"), + KRAD_SERVICE_TYPE_AUTHENTICATE_ONLY); + if (retval != 0) { + otpd_log_err(retval, "Unable to set Service-Type"); + goto error; + } + + /* Radius Client */ + retval = krad_client_new(ctx.kctx, ctx.vctx, &ctx.client); + if (retval != 0) { + otpd_log_err(retval, "Unable to initialize radius client"); + goto error; + } + + /* Signals */ + sig = verto_add_signal(ctx.vctx, VERTO_EV_FLAG_NONE, on_signal, SIGTERM); + if (sig == NULL) { + otpd_log_err(ENOMEM, "Unable to initialize signal event"); + goto error; + } + sig = verto_add_signal(ctx.vctx, VERTO_EV_FLAG_NONE, on_signal, SIGINT); + if (sig == NULL) { + otpd_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, + otpd_on_stdin_readable, STDIN_FILENO); + if (ctx.stdio.reader == NULL) { + otpd_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, + otpd_on_stdout_writable, STDOUT_FILENO); + if (ctx.stdio.writer == NULL) { + otpd_log_err(ENOMEM, "Unable to initialize writer event"); + goto error; + } + + /* LDAP (Query) */ + retval = setup_ldap(argv[1], TRUE, otpd_on_query_io, + &ctx.query.io, &ctx.query.base); + if (retval != 0) { + otpd_log_err(retval, "Unable to initialize LDAP (Query)"); + goto error; + } + + /* LDAP (Bind) */ + retval = setup_ldap(argv[1], FALSE, otpd_on_bind_io, + &ctx.bind.io, NULL); + if (retval != 0) { + otpd_log_err(retval, "Unable to initialize LDAP (Bind)"); + goto error; + } + + ctx.exitstatus = 0; + verto_run(ctx.vctx); + +error: + krad_client_free(ctx.client); + otpd_queue_free_items(&ctx.stdio.responses); + otpd_queue_free_items(&ctx.query.requests); + otpd_queue_free_items(&ctx.query.responses); + otpd_queue_free_items(&ctx.bind.requests); + otpd_queue_free_items(&ctx.bind.responses); + free(ctx.query.base); + 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 00000000..062b6403 --- /dev/null +++ b/daemons/ipa-otpd/parse.c @@ -0,0 +1,176 @@ +/* + * FreeIPA 2FA companion daemon + * + * Authors: Nathaniel McCallum + * + * 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 . + */ + +/* + * This file parses the user's configuration received from LDAP (see query.c). + */ + +#include "internal.h" +#include + +#define DEFAULT_TIMEOUT 15 +#define DEFAULT_RETRIES 3 + +/* Convert an LDAP entry into an allocated string. */ +static int get_string(LDAP *ldp, LDAPMessage *entry, const char *name, + char **out) +{ + struct berval **vals; + ber_len_t i; + char *buf; + + vals = ldap_get_values_len(ldp, entry, name); + if (vals == NULL) + return ENOENT; + + buf = calloc(vals[0]->bv_len + 1, sizeof(char)); + if (buf == 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(buf); + ldap_value_free_len(vals); + return EINVAL; + } + + buf[i] = vals[0]->bv_val[i]; + } + + if (*out != NULL) + free(*out); + *out = buf; + ldap_value_free_len(vals); + return 0; +} + +/* Convert an LDAP entry into an unsigned long. */ +static int get_ulong(LDAP *ldp, LDAPMessage *entry, const char *name, + unsigned long *out) +{ + struct berval **vals; + char buffer[32]; + + vals = ldap_get_values_len(ldp, entry, name); + if (vals == NULL) + return ENOENT; + + if (vals[0]->bv_len > sizeof(buffer) - 1) { + ldap_value_free_len(vals); + return ERANGE; + } + + memcpy(buffer, vals[0]->bv_val, vals[0]->bv_len); + buffer[vals[0]->bv_len] = '\0'; + ldap_value_free_len(vals); + + *out = strtoul(buffer, NULL, 10); + if (*out == ULONG_MAX) + return errno; + + return 0; +} + +/* Parse basic user configuration. */ +const char *otpd_parse_user(LDAP *ldp, LDAPMessage *entry, + struct otpd_queue_item *item) +{ + int i, j; + + i = get_string(ldp, entry, "uid", &item->user.uid); + if (i != 0) + return strerror(i); + + i = get_string(ldp, entry, "ipatokenRadiusUserName", + &item->user.ipatokenRadiusUserName); + if (i != 0 && i != ENOENT) + return strerror(i); + + i = get_string(ldp, entry, "ipatokenRadiusConfigLink", + &item->user.ipatokenRadiusConfigLink); + if (i != 0 && i != ENOENT) + return strerror(i); + + /* Get the DN. */ + item->user.dn = ldap_get_dn(ldp, entry); + if (item->user.dn == NULL) { + i = ldap_get_option(ldp, LDAP_OPT_RESULT_CODE, &j); + return ldap_err2string(i == LDAP_OPT_SUCCESS ? j : i); + } + + return NULL; +} + +/* Parse the user's RADIUS configuration. */ +const char *otpd_parse_radius(LDAP *ldp, LDAPMessage *entry, + struct otpd_queue_item *item) +{ + unsigned long l; + int i; + + i = get_string(ldp, entry, "ipatokenRadiusServer", + &item->radius.ipatokenRadiusServer); + if (i != 0) + return strerror(i); + + i = get_string(ldp, entry, "ipatokenRadiusSecret", + &item->radius.ipatokenRadiusSecret); + if (i != 0) + return strerror(i); + + i = get_string(ldp, entry, "ipatokenUserMapAttribute", + &item->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); + item->radius.ipatokenRadiusTimeout = l * 1000; + + i = get_ulong(ldp, entry, "ipatokenRadiusRetries", &l); + if (i == ENOENT) + l = DEFAULT_RETRIES; + else if (i != 0) + return strerror(i); + item->radius.ipatokenRadiusRetries = l; + + return NULL; +} + +/* Parse the user's RADIUS username. */ +const char *otpd_parse_radius_username(LDAP *ldp, LDAPMessage *entry, + struct otpd_queue_item *item) +{ + int i; + + i = get_string(ldp, entry, item->radius.ipatokenUserMapAttribute, + &item->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 00000000..67e2d751 --- /dev/null +++ b/daemons/ipa-otpd/query.c @@ -0,0 +1,253 @@ +/* + * FreeIPA 2FA companion daemon + * + * Authors: Nathaniel McCallum + * + * 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 . + */ + +/* + * This file receives requests (from stdio.c) and queries the LDAP server for + * the user's configuration. When the user's configuration is received, it is + * parsed (parse.c). Once the configuration is parsed, the request packet is + * either forwarded to a third-party RADIUS server (forward.c) or authenticated + * directly via an LDAP bind (bind.c) based on the configuration received. + */ + +#define _GNU_SOURCE 1 /* for asprintf() */ +#include "internal.h" +#include + +#define DEFAULT_TIMEOUT 15 +#define DEFAULT_RETRIES 3 + +static char *user[] = { + "uid", + "ipatokenRadiusUserName", + "ipatokenRadiusConfigLink", + NULL +}; + +static char *radius[] = { + "ipatokenRadiusServer", + "ipatokenRadiusSecret", + "ipatokenRadiusTimeout", + "ipatokenRadiusRetries", + "ipatokenUserMapAttribute", + NULL +}; + +/* Send queued LDAP requests to the server. */ +static void on_query_writable(verto_ctx *vctx, verto_ev *ev) +{ + struct otpd_queue *push = &ctx.stdio.responses; + const krb5_data *princ = NULL; + char *filter = NULL, *attrs[2]; + int i = LDAP_SUCCESS; + struct otpd_queue_item *item; + (void)vctx; + + item = otpd_queue_pop(&ctx.query.requests); + if (item == NULL) { + verto_set_flags(ctx.query.io, VERTO_EV_FLAG_PERSIST | + VERTO_EV_FLAG_IO_ERROR | + VERTO_EV_FLAG_IO_READ); + return; + } + + if (item->user.dn == NULL) { + princ = krad_packet_get_attr(item->req, + krad_attr_name2num("User-Name"), 0); + if (princ == NULL) + goto error; + + otpd_log_req(item->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, &item->msgid); + free(filter); + + } else if (item->radius.ipatokenRadiusSecret == NULL) { + otpd_log_req(item->req, "radius query start: %s", + item->user.ipatokenRadiusConfigLink); + + i = ldap_search_ext(verto_get_private(ev), + item->user.ipatokenRadiusConfigLink, + LDAP_SCOPE_BASE, NULL, radius, 0, NULL, + NULL, NULL, 1, &item->msgid); + + } else if (item->radius.ipatokenUserMapAttribute != NULL) { + otpd_log_req(item->req, "username query start: %s", + item->radius.ipatokenUserMapAttribute); + + attrs[0] = item->radius.ipatokenUserMapAttribute; + attrs[1] = NULL; + i = ldap_search_ext(verto_get_private(ev), item->user.dn, + LDAP_SCOPE_BASE, NULL, attrs, 0, NULL, + NULL, NULL, 1, &item->msgid); + } + + if (i == LDAP_SUCCESS) { + item->sent++; + push = &ctx.query.responses; + } + +error: + otpd_queue_push(push, item); +} + +/* Read LDAP responses from the server. */ +static void on_query_readable(verto_ctx *vctx, verto_ev *ev) +{ + struct otpd_queue *push = &ctx.stdio.responses; + verto_ev *event = ctx.stdio.writer; + LDAPMessage *results, *entry; + struct otpd_queue_item *item = NULL; + const char *err; + 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; + } + + item = otpd_queue_pop_msgid(&ctx.query.responses, ldap_msgid(results)); + if (item == NULL) + goto egress; + + if (i == LDAP_RES_SEARCH_ENTRY) { + entry = ldap_first_entry(ldp, results); + if (entry == NULL) + goto egress; + + err = NULL; + switch (item->sent) { + case 1: + err = otpd_parse_user(ldp, entry, item); + break; + case 2: + err = otpd_parse_radius(ldp, entry, item); + break; + case 3: + err = otpd_parse_radius_username(ldp, entry, item); + break; + default: + ldap_msgfree(entry); + goto egress; + } + + ldap_msgfree(entry); + + if (err != NULL) { + if (item->error != NULL) + free(item->error); + item->error = strdup(err); + if (item->error == NULL) + goto egress; + } + + otpd_queue_push_head(&ctx.query.responses, item); + return; + } + + item->msgid = -1; + + switch (item->sent) { + case 1: + otpd_log_req(item->req, "user query end: %s", + item->error == NULL ? item->user.dn : item->error); + if (item->user.dn == NULL || item->user.uid == NULL) + goto egress; + break; + case 2: + otpd_log_req(item->req, "radius query end: %s", + item->error == NULL + ? item->radius.ipatokenRadiusServer + : item->error); + if (item->radius.ipatokenRadiusServer == NULL || + item->radius.ipatokenRadiusSecret == NULL) + goto egress; + break; + case 3: + otpd_log_req(item->req, "username query end: %s", + item->error == NULL ? item->user.other : item->error); + break; + default: + goto egress; + } + + if (item->error != NULL) + goto egress; + + if (item->sent == 1 && item->user.ipatokenRadiusConfigLink != NULL) { + push = &ctx.query.requests; + event = ctx.query.io; + goto egress; + } else if (item->sent == 2 && + item->radius.ipatokenUserMapAttribute != NULL && + item->user.ipatokenRadiusUserName == NULL) { + push = &ctx.query.requests; + event = ctx.query.io; + goto egress; + } + + /* Forward to RADIUS if necessary. */ + i = otpd_forward(&item); + if (i != 0) + goto egress; + + push = &ctx.bind.requests; + event = ctx.bind.io; + +egress: + ldap_msgfree(results); + otpd_queue_push(push, item); + + if (item != NULL) + verto_set_flags(event, VERTO_EV_FLAG_PERSIST | + VERTO_EV_FLAG_IO_ERROR | + VERTO_EV_FLAG_IO_READ | + VERTO_EV_FLAG_IO_WRITE); +} + +/* Handle the reading/writing of LDAP query requests asynchronously. */ +void otpd_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) { + otpd_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 00000000..730bbc40 --- /dev/null +++ b/daemons/ipa-otpd/queue.c @@ -0,0 +1,183 @@ +/* + * FreeIPA 2FA companion daemon + * + * Authors: Nathaniel McCallum + * + * 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 . + */ + +/* + * This file contains an implementation of a queue of request/response items. + */ + +#include "internal.h" + +struct otpd_queue_iter { + struct otpd_queue_item *next; + unsigned int qindx; + const struct otpd_queue * const *queues; +}; + +krb5_error_code otpd_queue_item_new(krad_packet *req, + struct otpd_queue_item **item) +{ + *item = calloc(1, sizeof(struct otpd_queue_item)); + if (*item == NULL) + return ENOMEM; + + (*item)->req = req; + (*item)->msgid = -1; + return 0; +} + +void otpd_queue_item_free(struct otpd_queue_item *item) +{ + if (item == NULL) + return; + + ldap_memfree(item->user.dn); + free(item->user.uid); + free(item->user.ipatokenRadiusUserName); + free(item->user.ipatokenRadiusConfigLink); + free(item->user.other); + free(item->radius.ipatokenRadiusServer); + free(item->radius.ipatokenRadiusSecret); + free(item->radius.ipatokenUserMapAttribute); + free(item->error); + krad_packet_free(item->req); + krad_packet_free(item->rsp); + free(item); +} + +krb5_error_code otpd_queue_iter_new(const struct otpd_queue * const *queues, + struct otpd_queue_iter **iter) +{ + *iter = calloc(1, sizeof(struct otpd_queue_iter)); + if (*iter == NULL) + return ENOMEM; + + (*iter)->queues = queues; + return 0; +} + +/* This iterator function is used by krad to loop over all outstanding requests + * to check for duplicates. Hence, we have to iterate over all the queues to + * return all the outstanding requests as a flat list. */ +const krad_packet *otpd_queue_iter_func(void *data, krb5_boolean cancel) +{ + struct otpd_queue_iter *iter = data; + const struct otpd_queue *q; + + if (cancel) { + free(iter); + return NULL; + } + + if (iter->next != NULL) { + struct otpd_queue_item *tmp; + tmp = iter->next; + iter->next = tmp->next; + return tmp->req; + } + + q = iter->queues[iter->qindx++]; + if (q == NULL) + return otpd_queue_iter_func(data, TRUE); + + iter->next = q->head; + return otpd_queue_iter_func(data, FALSE); +} + +void otpd_queue_push(struct otpd_queue *q, struct otpd_queue_item *item) +{ + if (item == NULL) + return; + + if (q->tail == NULL) + q->head = q->tail = item; + else + q->tail = q->tail->next = item; +} + +void otpd_queue_push_head(struct otpd_queue *q, struct otpd_queue_item *item) +{ + if (item == NULL) + return; + + if (q->head == NULL) + q->tail = q->head = item; + else { + item->next = q->head; + q->head = item; + } +} + +struct otpd_queue_item *otpd_queue_peek(struct otpd_queue *q) +{ + return q->head; +} + +struct otpd_queue_item *otpd_queue_pop(struct otpd_queue *q) +{ + struct otpd_queue_item *item; + + if (q == NULL) + return NULL; + + item = q->head; + if (item != NULL) + q->head = item->next; + + if (q->head == NULL) + q->tail = NULL; + + return item; +} + +/* Remove and return an item from the queue with the given msgid. */ +struct otpd_queue_item *otpd_queue_pop_msgid(struct otpd_queue *q, int msgid) +{ + struct otpd_queue_item *item, **prev; + + for (item = q->head, prev = &q->head; + item != NULL; + item = item->next, prev = &item->next) { + if (item->msgid == msgid) { + *prev = item->next; + if (q->head == NULL) + q->tail = NULL; + return item; + } + } + + return NULL; +} + +void otpd_queue_free_items(struct otpd_queue *q) +{ + struct otpd_queue_item *item, *next; + + next = q->head; + while (next != NULL) { + item = next; + next = next->next; + otpd_queue_item_free(item); + } + + 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 00000000..ac51c78d --- /dev/null +++ b/daemons/ipa-otpd/stdio.c @@ -0,0 +1,205 @@ +/* + * FreeIPA 2FA companion daemon + * + * Authors: Nathaniel McCallum + * + * 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 . + */ + +/* + * This file reads and writes RADIUS packets on STDIN/STDOUT. + * + * Incoming requests are placed into a "query" queue to look up the user's + * configuration from LDAP (query.c). + */ + +#include "internal.h" + +static const struct otpd_queue *const queues[] = { + &ctx.stdio.responses, + &ctx.query.requests, + &ctx.query.responses, + &ctx.bind.requests, + &ctx.bind.responses, + NULL +}; + +/* Read a RADIUS request from stdin. */ +void otpd_on_stdin_readable(verto_ctx *vctx, verto_ev *ev) +{ + static char _buffer[KRAD_PACKET_SIZE_MAX]; + static krb5_data buffer = { .data = _buffer, .length = 0 }; + (void)vctx; + + const krad_packet *dup; + const krb5_data *data; + struct otpd_queue_iter *iter; + struct otpd_queue_item *item; + krad_packet *req; + ssize_t pktlen; + int i; + + pktlen = krad_packet_bytes_needed(&buffer); + if (pktlen < 0) { + otpd_log_err(EBADMSG, "Received a malformed packet"); + 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) { + otpd_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 = krad_packet_bytes_needed(&buffer); + if (pktlen > 0) + return; + + /* Create the iterator. */ + i = otpd_queue_iter_new(queues, &iter); + if (i != 0) { + otpd_log_err(i, "Unable to create iterator"); + goto shutdown; + } + + /* Decode the item. */ + i = krad_packet_decode_request(ctx.kctx, SECRET, &buffer, + otpd_queue_iter_func, iter, &dup, &req); + buffer.length = 0; + if (i == EAGAIN) + return; + else if (i != 0) { + otpd_log_err(i, "Unable to decode item"); + goto shutdown; + } + + /* Drop duplicate requests. */ + if (dup != NULL) { + krad_packet_free(req); + return; + } + + /* Ensure the packet has the User-Name attribute. */ + data = krad_packet_get_attr(req, krad_attr_name2num("User-Name"), 0); + if (data == NULL) { + krad_packet_free(req); + return; + } + + /* Create the new queue item. */ + i = otpd_queue_item_new(req, &item); + if (i != 0) { + krad_packet_free(req); + return; + } + + /* Push it to the query queue. */ + otpd_queue_push(&ctx.query.requests, item); + 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); + + otpd_log_req(req, "request received"); + return; + +shutdown: + verto_break(ctx.vctx); + ctx.exitstatus = 1; +} + +/* Send a RADIUS response to stdout. */ +void otpd_on_stdout_writable(verto_ctx *vctx, verto_ev *ev) +{ + const krb5_data *data; + struct otpd_queue_item *item; + int i; + (void)vctx; + + item = otpd_queue_peek(&ctx.stdio.responses); + if (item == 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 (item->rsp == NULL) { + item->sent = 0; + i = krad_packet_new_response(ctx.kctx, SECRET, + krad_code_name2num("Access-Reject"), + NULL, item->req, &item->rsp); + if (i != 0) { + otpd_log_err(errno, "Unable to craft response"); + goto shutdown; + } + } + + /* Send the packet. */ + data = krad_packet_encode(item->rsp); + i = write(verto_get_fd(ev), data->data + item->sent, + data->length - item->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; + } + + otpd_log_err(errno, "Error writing to stdout!"); + goto shutdown; + } + + /* If the packet was completely sent, free the response. */ + item->sent += i; + if (item->sent == data->length) { + otpd_log_req(item->req, "response sent: %s", + krad_code_num2name(krad_packet_get_code(item->rsp))); + otpd_queue_item_free(otpd_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 00000000..d748c825 --- /dev/null +++ b/daemons/ipa-otpd/test.py @@ -0,0 +1,61 @@ +#!/usr/bin/python +# +# FreeIPA 2FA companion daemon +# +# Authors: Nathaniel McCallum +# +# 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 . + +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() #pylint: disable=E1101 +proc.wait() -- cgit