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