Path: blob/main/crypto/krb5/src/plugins/preauth/otp/otp_state.c
34907 views
/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */1/* plugins/preauth/otp/otp_state.c - Verify OTP token values using RADIUS */2/*3* Copyright 2013 Red Hat, Inc. All rights reserved.4*5* Redistribution and use in source and binary forms, with or without6* modification, are permitted provided that the following conditions are met:7*8* 1. Redistributions of source code must retain the above copyright9* notice, this list of conditions and the following disclaimer.10*11* 2. Redistributions in binary form must reproduce the above copyright12* notice, this list of conditions and the following disclaimer in13* the documentation and/or other materials provided with the14* distribution.15*16* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS17* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED18* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A19* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER20* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,21* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,22* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR23* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF24* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING25* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS26* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.27*/2829#include "otp_state.h"3031#include <krad.h>32#include <k5-json.h>3334#include <ctype.h>3536#ifndef HOST_NAME_MAX37/* SUSv2 */38#define HOST_NAME_MAX 25539#endif4041#define DEFAULT_TYPE_NAME "DEFAULT"42#define DEFAULT_SOCKET_FMT KDC_RUN_DIR "/%s.socket"43#define DEFAULT_TIMEOUT 544#define DEFAULT_RETRIES 345#define MAX_SECRET_LEN 10244647typedef struct token_type_st {48char *name;49char *server;50char *secret;51int timeout;52size_t retries;53krb5_boolean strip_realm;54char **indicators;55} token_type;5657typedef struct token_st {58const token_type *type;59krb5_data username;60char **indicators;61} token;6263typedef struct request_st {64otp_state *state;65token *tokens;66ssize_t index;67otp_cb cb;68void *data;69krad_attrset *attrs;70} request;7172struct otp_state_st {73krb5_context ctx;74token_type *types;75krad_client *radius;76krad_attrset *attrs;77};7879static void request_send(request *req);8081static krb5_error_code82read_secret_file(const char *secret_file, char **secret)83{84char buf[MAX_SECRET_LEN];85krb5_error_code retval;86char *filename = NULL;87FILE *file;88size_t i, j;8990*secret = NULL;9192retval = k5_path_join(KDC_DIR, secret_file, &filename);93if (retval != 0) {94com_err("otp", retval, "Unable to resolve secret file '%s'", filename);95goto cleanup;96}9798file = fopen(filename, "r");99if (file == NULL) {100retval = errno;101com_err("otp", retval, "Unable to open secret file '%s'", filename);102goto cleanup;103}104105if (fgets(buf, sizeof(buf), file) == NULL)106retval = EIO;107fclose(file);108if (retval != 0) {109com_err("otp", retval, "Unable to read secret file '%s'", filename);110goto cleanup;111}112113/* Strip whitespace. */114for (i = 0; buf[i] != '\0'; i++) {115if (!isspace(buf[i]))116break;117}118for (j = strlen(buf); j > i; j--) {119if (!isspace(buf[j - 1]))120break;121}122123*secret = k5memdup0(&buf[i], j - i, &retval);124125cleanup:126free(filename);127return retval;128}129130/* Free the contents of a single token type. */131static void132token_type_free(token_type *type)133{134if (type == NULL)135return;136137free(type->name);138free(type->server);139free(type->secret);140profile_free_list(type->indicators);141}142143/* Construct the internal default token type. */144static krb5_error_code145token_type_default(token_type *out)146{147char *name = NULL, *server = NULL, *secret = NULL;148149memset(out, 0, sizeof(*out));150151name = strdup(DEFAULT_TYPE_NAME);152if (name == NULL)153goto oom;154if (asprintf(&server, DEFAULT_SOCKET_FMT, name) < 0)155goto oom;156secret = strdup("");157if (secret == NULL)158goto oom;159160out->name = name;161out->server = server;162out->secret = secret;163out->timeout = DEFAULT_TIMEOUT * 1000;164out->retries = DEFAULT_RETRIES;165out->strip_realm = FALSE;166return 0;167168oom:169free(name);170free(server);171free(secret);172return ENOMEM;173}174175/* Decode a single token type from the profile. */176static krb5_error_code177token_type_decode(profile_t profile, const char *name, token_type *out)178{179char *server = NULL, *name_copy = NULL, *secret = NULL, *pstr = NULL;180char **indicators = NULL;181const char *keys[4];182int strip_realm, timeout, retries;183krb5_error_code retval;184185memset(out, 0, sizeof(*out));186187/* Set the name. */188name_copy = strdup(name);189if (name_copy == NULL)190return ENOMEM;191192/* Set strip_realm. */193retval = profile_get_boolean(profile, "otp", name, "strip_realm", TRUE,194&strip_realm);195if (retval != 0)196goto cleanup;197198/* Set the server. */199retval = profile_get_string(profile, "otp", name, "server", NULL, &pstr);200if (retval != 0)201goto cleanup;202if (pstr != NULL) {203server = strdup(pstr);204profile_release_string(pstr);205if (server == NULL) {206retval = ENOMEM;207goto cleanup;208}209} else if (asprintf(&server, DEFAULT_SOCKET_FMT, name) < 0) {210retval = ENOMEM;211goto cleanup;212}213214/* Get the secret (optional for Unix-domain sockets). */215retval = profile_get_string(profile, "otp", name, "secret", NULL, &pstr);216if (retval != 0)217goto cleanup;218if (pstr != NULL) {219retval = read_secret_file(pstr, &secret);220profile_release_string(pstr);221if (retval != 0)222goto cleanup;223} else {224if (server[0] != '/') {225com_err("otp", EINVAL, "Secret missing (token type '%s')", name);226retval = EINVAL;227goto cleanup;228}229230/* Use the default empty secret for UNIX domain stream sockets. */231secret = strdup("");232if (secret == NULL) {233retval = ENOMEM;234goto cleanup;235}236}237238/* Get the timeout (profile value in seconds, result in milliseconds). */239retval = profile_get_integer(profile, "otp", name, "timeout",240DEFAULT_TIMEOUT, &timeout);241if (retval != 0)242goto cleanup;243timeout *= 1000;244245/* Get the number of retries. */246retval = profile_get_integer(profile, "otp", name, "retries",247DEFAULT_RETRIES, &retries);248if (retval != 0)249goto cleanup;250251/* Get the authentication indicators to assert if this token is used. */252keys[0] = "otp";253keys[1] = name;254keys[2] = "indicator";255keys[3] = NULL;256retval = profile_get_values(profile, keys, &indicators);257if (retval == PROF_NO_RELATION)258retval = 0;259if (retval != 0)260goto cleanup;261262out->name = name_copy;263out->server = server;264out->secret = secret;265out->timeout = timeout;266out->retries = retries;267out->strip_realm = strip_realm;268out->indicators = indicators;269name_copy = server = secret = NULL;270indicators = NULL;271272cleanup:273free(name_copy);274free(server);275free(secret);276profile_free_list(indicators);277return retval;278}279280/* Free an array of token types. */281static void282token_types_free(token_type *types)283{284size_t i;285286if (types == NULL)287return;288289for (i = 0; types[i].server != NULL; i++)290token_type_free(&types[i]);291292free(types);293}294295/* Decode an array of token types from the profile. */296static krb5_error_code297token_types_decode(profile_t profile, token_type **out)298{299const char *hier[2] = { "otp", NULL };300token_type *types = NULL;301char **names = NULL;302krb5_error_code retval;303size_t i, pos;304krb5_boolean have_default = FALSE;305306retval = profile_get_subsection_names(profile, hier, &names);307if (retval != 0)308return retval;309310/* Check if any of the profile subsections overrides the default. */311for (i = 0; names[i] != NULL; i++) {312if (strcmp(names[i], DEFAULT_TYPE_NAME) == 0)313have_default = TRUE;314}315316/* Leave space for the default (possibly) and the terminator. */317types = k5calloc(i + 2, sizeof(token_type), &retval);318if (types == NULL)319goto cleanup;320321/* If no default has been specified, use our internal default. */322pos = 0;323if (!have_default) {324retval = token_type_default(&types[pos++]);325if (retval != 0)326goto cleanup;327}328329/* Decode each profile section into a token type element. */330for (i = 0; names[i] != NULL; i++) {331retval = token_type_decode(profile, names[i], &types[pos++]);332if (retval != 0)333goto cleanup;334}335336*out = types;337types = NULL;338339cleanup:340profile_free_list(names);341token_types_free(types);342return retval;343}344345/* Free a null-terminated array of strings. */346static void347free_strings(char **list)348{349char **p;350351for (p = list; p != NULL && *p != NULL; p++)352free(*p);353free(list);354}355356/* Free the contents of a single token. */357static void358token_free_contents(token *t)359{360if (t == NULL)361return;362free(t->username.data);363free_strings(t->indicators);364}365366/* Decode a JSON array of strings into a null-terminated list of C strings. */367static krb5_error_code368indicators_decode(krb5_context ctx, k5_json_value val, char ***indicators_out)369{370k5_json_array arr;371k5_json_value obj;372char **indicators;373size_t len, i;374375*indicators_out = NULL;376377if (k5_json_get_tid(val) != K5_JSON_TID_ARRAY)378return EINVAL;379arr = val;380len = k5_json_array_length(arr);381indicators = calloc(len + 1, sizeof(*indicators));382if (indicators == NULL)383return ENOMEM;384385for (i = 0; i < len; i++) {386obj = k5_json_array_get(arr, i);387if (k5_json_get_tid(obj) != K5_JSON_TID_STRING) {388free_strings(indicators);389return EINVAL;390}391indicators[i] = strdup(k5_json_string_utf8(obj));392if (indicators[i] == NULL) {393free_strings(indicators);394return ENOMEM;395}396}397398*indicators_out = indicators;399return 0;400}401402/* Decode a single token from a JSON token object. */403static krb5_error_code404token_decode(krb5_context ctx, krb5_const_principal princ,405const token_type *types, k5_json_object obj, token *out)406{407const char *typename = DEFAULT_TYPE_NAME;408const token_type *type = NULL;409char *username = NULL, **indicators = NULL;410krb5_error_code retval;411k5_json_value val;412size_t i;413int flags;414415memset(out, 0, sizeof(*out));416417/* Find the token type. */418val = k5_json_object_get(obj, "type");419if (val != NULL && k5_json_get_tid(val) == K5_JSON_TID_STRING)420typename = k5_json_string_utf8(val);421for (i = 0; types[i].server != NULL; i++) {422if (strcmp(typename, types[i].name) == 0)423type = &types[i];424}425if (type == NULL)426return EINVAL;427428/* Get the username, either from obj or from unparsing the principal. */429val = k5_json_object_get(obj, "username");430if (val != NULL && k5_json_get_tid(val) == K5_JSON_TID_STRING) {431username = strdup(k5_json_string_utf8(val));432if (username == NULL)433return ENOMEM;434} else {435flags = type->strip_realm ? KRB5_PRINCIPAL_UNPARSE_NO_REALM : 0;436retval = krb5_unparse_name_flags(ctx, princ, flags, &username);437if (retval != 0)438return retval;439}440441/* Get the authentication indicators if specified. */442val = k5_json_object_get(obj, "indicators");443if (val != NULL) {444retval = indicators_decode(ctx, val, &indicators);445if (retval != 0) {446free(username);447return retval;448}449}450451out->type = type;452out->username = string2data(username);453out->indicators = indicators;454return 0;455}456457/* Free an array of tokens. */458static void459tokens_free(token *tokens)460{461size_t i;462463if (tokens == NULL)464return;465466for (i = 0; tokens[i].type != NULL; i++)467token_free_contents(&tokens[i]);468469free(tokens);470}471472/* Decode a principal config string into a JSON array. Treat an empty string473* or array as if it were "[{}]" which uses the default token type. */474static krb5_error_code475decode_config_json(const char *config, k5_json_array *out)476{477krb5_error_code retval;478k5_json_value val;479k5_json_object obj;480481*out = NULL;482483/* Decode the config string and make sure it's an array. */484retval = k5_json_decode((config != NULL) ? config : "[{}]", &val);485if (retval != 0)486goto error;487if (k5_json_get_tid(val) != K5_JSON_TID_ARRAY) {488retval = EINVAL;489goto error;490}491492/* If the array is empty, add in an empty object. */493if (k5_json_array_length(val) == 0) {494retval = k5_json_object_create(&obj);495if (retval != 0)496goto error;497retval = k5_json_array_add(val, obj);498k5_json_release(obj);499if (retval != 0)500goto error;501}502503*out = val;504return 0;505506error:507k5_json_release(val);508return retval;509}510511/* Decode an array of tokens from the configuration string. */512static krb5_error_code513tokens_decode(krb5_context ctx, krb5_const_principal princ,514const token_type *types, const char *config, token **out)515{516krb5_error_code retval;517k5_json_array arr = NULL;518k5_json_value obj;519token *tokens = NULL;520size_t len, i;521522retval = decode_config_json(config, &arr);523if (retval != 0)524return retval;525len = k5_json_array_length(arr);526527tokens = k5calloc(len + 1, sizeof(token), &retval);528if (tokens == NULL)529goto cleanup;530531for (i = 0; i < len; i++) {532obj = k5_json_array_get(arr, i);533if (k5_json_get_tid(obj) != K5_JSON_TID_OBJECT) {534retval = EINVAL;535goto cleanup;536}537retval = token_decode(ctx, princ, types, obj, &tokens[i]);538if (retval != 0)539goto cleanup;540}541542*out = tokens;543tokens = NULL;544545cleanup:546k5_json_release(arr);547tokens_free(tokens);548return retval;549}550551static void552request_free(request *req)553{554if (req == NULL)555return;556557krad_attrset_free(req->attrs);558tokens_free(req->tokens);559free(req);560}561562krb5_error_code563otp_state_new(krb5_context ctx, otp_state **out)564{565char hostname[HOST_NAME_MAX + 1];566krb5_error_code retval;567profile_t profile;568krb5_data hndata;569otp_state *self;570571retval = gethostname(hostname, sizeof(hostname));572if (retval != 0)573return retval;574575self = calloc(1, sizeof(otp_state));576if (self == NULL)577return ENOMEM;578579retval = krb5_get_profile(ctx, &profile);580if (retval != 0)581goto error;582583retval = token_types_decode(profile, &self->types);584profile_abandon(profile);585if (retval != 0)586goto error;587588retval = krad_attrset_new(ctx, &self->attrs);589if (retval != 0)590goto error;591592hndata = make_data(hostname, strlen(hostname));593retval = krad_attrset_add(self->attrs, KRAD_ATTR_NAS_IDENTIFIER, &hndata);594if (retval != 0)595goto error;596597retval = krad_attrset_add_number(self->attrs, KRAD_ATTR_SERVICE_TYPE,598KRAD_SERVICE_TYPE_AUTHENTICATE_ONLY);599if (retval != 0)600goto error;601602self->ctx = ctx;603*out = self;604return 0;605606error:607otp_state_free(self);608return retval;609}610611void612otp_state_free(otp_state *self)613{614if (self == NULL)615return;616617krad_attrset_free(self->attrs);618krad_client_free(self->radius);619token_types_free(self->types);620free(self);621}622623static void624callback(krb5_error_code retval, const krad_packet *rqst,625const krad_packet *resp, void *data)626{627request *req = data;628token *tok = &req->tokens[req->index];629char *const *indicators;630631req->index++;632633if (retval != 0)634goto error;635636/* If we received an accept packet, success! */637if (krad_packet_get_code(resp) == KRAD_CODE_ACCESS_ACCEPT) {638indicators = tok->indicators;639if (indicators == NULL)640indicators = tok->type->indicators;641req->cb(req->data, retval, otp_response_success, indicators);642request_free(req);643return;644}645646/* If we have no more tokens to try, failure! */647if (req->tokens[req->index].type == NULL)648goto error;649650/* Try the next token. */651request_send(req);652return;653654error:655req->cb(req->data, retval, otp_response_fail, NULL);656request_free(req);657}658659static void660request_send(request *req)661{662krb5_error_code retval;663token *tok = &req->tokens[req->index];664const token_type *t = tok->type;665666retval = krad_attrset_add(req->attrs, KRAD_ATTR_USER_NAME, &tok->username);667if (retval != 0)668goto error;669670retval = krad_client_send(req->state->radius, KRAD_CODE_ACCESS_REQUEST,671req->attrs, t->server, t->secret, t->timeout,672t->retries, callback, req);673krad_attrset_del(req->attrs, KRAD_ATTR_USER_NAME, 0);674if (retval != 0)675goto error;676677return;678679error:680req->cb(req->data, retval, otp_response_fail, NULL);681request_free(req);682}683684void685otp_state_verify(otp_state *state, verto_ctx *ctx, krb5_const_principal princ,686const char *config, const krb5_pa_otp_req *req,687otp_cb cb, void *data)688{689krb5_error_code retval;690request *rqst = NULL;691char *name;692693if (state->radius == NULL) {694retval = krad_client_new(state->ctx, ctx, &state->radius);695if (retval != 0)696goto error;697}698699rqst = calloc(1, sizeof(request));700if (rqst == NULL) {701(*cb)(data, ENOMEM, otp_response_fail, NULL);702return;703}704rqst->state = state;705rqst->data = data;706rqst->cb = cb;707708retval = krad_attrset_copy(state->attrs, &rqst->attrs);709if (retval != 0)710goto error;711712retval = krad_attrset_add(rqst->attrs, KRAD_ATTR_USER_PASSWORD,713&req->otp_value);714if (retval != 0)715goto error;716717retval = tokens_decode(state->ctx, princ, state->types, config,718&rqst->tokens);719if (retval != 0) {720if (krb5_unparse_name(state->ctx, princ, &name) == 0) {721com_err("otp", retval,722"Can't decode otp config string for principal '%s'", name);723krb5_free_unparsed_name(state->ctx, name);724}725goto error;726}727728request_send(rqst);729return;730731error:732(*cb)(data, retval, otp_response_fail, NULL);733request_free(rqst);734}735736737