diff options
Diffstat (limited to 'src/plugins/preauth/otp/otp_state.c')
| -rw-r--r-- | src/plugins/preauth/otp/otp_state.c | 735 |
1 files changed, 735 insertions, 0 deletions
diff --git a/src/plugins/preauth/otp/otp_state.c b/src/plugins/preauth/otp/otp_state.c new file mode 100644 index 000000000000..5ba3d917aff1 --- /dev/null +++ b/src/plugins/preauth/otp/otp_state.c @@ -0,0 +1,735 @@ +/* -*- 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_RUN_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; + char **indicators; +} token_type; + +typedef struct token_st { + const token_type *type; + krb5_data username; + char **indicators; +} 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); + profile_free_list(type->indicators); +} + +/* 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; + char **indicators = NULL; + const char *keys[4]; + 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; + + /* Get the authentication indicators to assert if this token is used. */ + keys[0] = "otp"; + keys[1] = name; + keys[2] = "indicator"; + keys[3] = NULL; + retval = profile_get_values(profile, keys, &indicators); + if (retval == PROF_NO_RELATION) + retval = 0; + 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; + out->indicators = indicators; + name_copy = server = secret = NULL; + indicators = NULL; + +cleanup: + free(name_copy); + free(server); + free(secret); + profile_free_list(indicators); + 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 = k5calloc(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 a null-terminated array of strings. */ +static void +free_strings(char **list) +{ + char **p; + + for (p = list; p != NULL && *p != NULL; p++) + free(*p); + free(list); +} + +/* Free the contents of a single token. */ +static void +token_free_contents(token *t) +{ + if (t == NULL) + return; + free(t->username.data); + free_strings(t->indicators); +} + +/* Decode a JSON array of strings into a null-terminated list of C strings. */ +static krb5_error_code +indicators_decode(krb5_context ctx, k5_json_value val, char ***indicators_out) +{ + k5_json_array arr; + k5_json_value obj; + char **indicators; + size_t len, i; + + *indicators_out = NULL; + + if (k5_json_get_tid(val) != K5_JSON_TID_ARRAY) + return EINVAL; + arr = val; + len = k5_json_array_length(arr); + indicators = calloc(len + 1, sizeof(*indicators)); + if (indicators == NULL) + return ENOMEM; + + for (i = 0; i < len; i++) { + obj = k5_json_array_get(arr, i); + if (k5_json_get_tid(obj) != K5_JSON_TID_STRING) { + free_strings(indicators); + return EINVAL; + } + indicators[i] = strdup(k5_json_string_utf8(obj)); + if (indicators[i] == NULL) { + free_strings(indicators); + return ENOMEM; + } + } + + *indicators_out = indicators; + return 0; +} + +/* 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, **indicators = 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; + } + + /* Get the authentication indicators if specified. */ + val = k5_json_object_get(obj, "indicators"); + if (val != NULL) { + retval = indicators_decode(ctx, val, &indicators); + if (retval != 0) { + free(username); + return retval; + } + } + + out->type = type; + out->username = string2data(username); + out->indicators = indicators; + 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 (retval != 0) + goto error; + 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 = k5calloc(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; + token *tok = &req->tokens[req->index]; + char *const *indicators; + + 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")) { + indicators = tok->indicators; + if (indicators == NULL) + indicators = tok->type->indicators; + req->cb(req->data, retval, otp_response_success, indicators); + 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, NULL); + 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, NULL); + 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, NULL); + 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, NULL); + request_free(rqst); +} |
