/** BEGIN COPYRIGHT BLOCK * 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; version 3 of the License. * * 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, write to the Free Software Foundation, Inc., 59 Temple * Place, Suite 330, Boston, MA 02111-1307 USA. * * In addition, as a special exception, Red Hat, Inc. gives You the additional * right to link the code of this Program with code not covered under the GNU * General Public License ("Non-GPL Code") and to distribute linked combinations * including the two, subject to the limitations in this paragraph. Non-GPL Code * permitted under this exception must only link to the code of this Program * through those well defined interfaces identified in the file named EXCEPTION * found in the source code files (the "Approved Interfaces"). The files of * Non-GPL Code may instantiate templates or use macros or inline functions from * the Approved Interfaces without causing the resulting work to be covered by * the GNU General Public License. Only Red Hat, Inc. may make changes or * additions to the list of Approved Interfaces. You must obey the GNU General * Public License in all respects for all of the Program code and other code used * in conjunction with the Program except the Non-GPL Code covered by this * exception. If you modify this file, you may extend this exception to your * version of the file, but you are not obligated to do so. If you do not wish to * provide this exception without modification, you must delete this exception * statement from your version and license this file solely under the GPL without * exception. * * * Copyright (C) 2013 Red Hat, Inc. * All rights reserved. * END COPYRIGHT BLOCK **/ #include "ipapwd.h" #define IPA_OTP_TOKEN_TOTP_OC "ipaTokenTOTP" #define IPA_OTP_DEFAULT_TOKEN_ALGORITHM "sha1" #define IPA_OTP_DEFAULT_TOKEN_OFFSET 0 #define IPA_OTP_DEFAULT_TOKEN_STEP 30 /* * From otp.c */ bool ipapwd_hotp(const uint8_t *key, size_t len, const char *algo, int digits, uint64_t counter, uint32_t *out); bool ipapwd_totp(const uint8_t *key, size_t len, const char *algo, int digits, time_t time, int offset, unsigned int step, uint32_t *out); /* From ipa_pwd_extop.c */ extern void *ipapwd_plugin_id; /* Data types. */ struct token { struct { uint8_t *data; size_t len; } key; char *algo; int len; union { struct { uint64_t counter; } hotp; struct { unsigned int step; int offset; } totp; }; bool (*auth)(const struct token *token, uint32_t otp); }; struct credentials { struct token token; Slapi_Value *ltp; uint32_t otp; }; static const char *valid_algos[] = { "sha1", "sha256", "sha384", "sha512", NULL }; static inline bool is_algo_valid(const char *algo) { int i, ret; for (i = 0; valid_algos[i]; i++) { ret = strcasecmp(algo, valid_algos[i]); if (ret == 0) return true; } return false; } static const struct berval *entry_attr_get_berval(const Slapi_Entry* e, const char *type) { Slapi_Attr* attr = NULL; Slapi_Value *v; int ret; ret = slapi_entry_attr_find(e, type, &attr); if (ret != 0 || attr == NULL) return NULL; ret = slapi_attr_first_value(attr, &v); if (ret < 0) return NULL; return slapi_value_get_berval(v); } /* Authenticate a totp token. Return zero on success. */ static bool auth_totp(const struct token *token, uint32_t otp) { time_t times[5]; uint32_t val; int i; /* Get the token value for now and two steps in either direction. */ times[0] = time(NULL); times[1] = times[0] + token->totp.step * 1; times[2] = times[0] - token->totp.step * 1; times[3] = times[0] + token->totp.step * 2; times[4] = times[0] - token->totp.step * 2; if (times[0] == -1) return false; /* Check all the times for a match. */ for (i = 0; i < sizeof(times) / sizeof(times[0]); i++) { if (!ipapwd_totp(token->key.data, token->key.len, token->algo, token->len, times[i], token->totp.offset, token->totp.step, &val)) { return false; } if (val == otp) { return true; } } return false; } static void token_free_contents(struct token *token) { if (token == NULL) return; slapi_ch_free_string(&token->algo); slapi_ch_free((void **) &token->key.data); } /* Decode an OTP token entry. Return zero on success. */ static bool token_decode(Slapi_Entry *te, struct token *token) { const struct berval *tmp; /* Get key. */ tmp = entry_attr_get_berval(te, IPA_OTP_TOKEN_KEY_TYPE); if (tmp == NULL) { slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME, "token_decode: key not set for token \"%s\".\n", slapi_entry_get_ndn(te)); return false; } token->key.len = tmp->bv_len; token->key.data = (void *) slapi_ch_malloc(token->key.len); memcpy(token->key.data, tmp->bv_val, token->key.len); /* Get length. */ token->len = slapi_entry_attr_get_int(te, IPA_OTP_TOKEN_LENGTH_TYPE); if (token->len < 6 || token->len > 10) { slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME, "token_decode: %s is not defined or invalid " "for token \"%s\".\n", IPA_OTP_TOKEN_LENGTH_TYPE, slapi_entry_get_ndn(te)); token_free_contents(token); return false; } /* Get algorithm. */ token->algo = slapi_entry_attr_get_charptr(te, IPA_OTP_TOKEN_ALGORITHM_TYPE); if (token->algo == NULL) token->algo = slapi_ch_strdup(IPA_OTP_DEFAULT_TOKEN_ALGORITHM); if (!is_algo_valid(token->algo)) { slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME, "token_decode: invalid token algorithm " "specified for token \"%s\".\n", slapi_entry_get_ndn(te)); token_free_contents(token); return false; } /* Currently, we only support TOTP. */ token->auth = auth_totp; /* Get offset. */ token->totp.offset = slapi_entry_attr_get_int(te, IPA_OTP_TOKEN_OFFSET_TYPE); if (token->totp.offset == 0) token->totp.offset = IPA_OTP_DEFAULT_TOKEN_OFFSET; /* Get step. */ token->totp.step = slapi_entry_attr_get_uint(te, IPA_OTP_TOKEN_STEP_TYPE); if (token->totp.step == 0) token->totp.step = IPA_OTP_DEFAULT_TOKEN_STEP; return true; } static void credentials_free_contents(struct credentials *credentials) { if (!credentials) return; token_free_contents(&credentials->token); slapi_value_free(&credentials->ltp); } /* Parse credentials and token entry. Return zero on success. */ static bool credentials_parse(Slapi_Entry *te, struct berval *creds, struct credentials *credentials) { char *tmp; int len; if (!token_decode(te, &credentials->token)) return false; /* Is the credential too short? If so, error. */ if (credentials->token.len >= creds->bv_len) { slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME, "credentials_parse: supplied credential is less " "than or equal to %s for token \"%s\".\n", IPA_OTP_TOKEN_LENGTH_TYPE, slapi_entry_get_ndn(te)); token_free_contents(&credentials->token); return false; } /* Extract the password from the supplied credential. We hand the * memory off to a Slapi_Value, so we don't want to directly free the * string. */ len = creds->bv_len - credentials->token.len; tmp = slapi_ch_calloc(len + 1, sizeof(char)); strncpy(tmp, creds->bv_val, len); credentials->ltp = slapi_value_new_string_passin(tmp); /* Extract the token value as a (minimum) 32-bit unsigned integer. */ tmp = slapi_ch_calloc(credentials->token.len + 1, sizeof(char)); strncpy(tmp, creds->bv_val + len, credentials->token.len); credentials->otp = strtoul(tmp, NULL, 10); slapi_ch_free_string(&tmp); return true; } /* * Attempts to perform OTP authentication for the passed in bind entry using * the passed in credentials. */ bool ipapwd_do_otp_auth(Slapi_Entry *bind_entry, struct berval *creds) { Slapi_PBlock *search_pb = NULL; Slapi_Value **pwd_vals = NULL; Slapi_Attr *pwd_attr = NULL; Slapi_Entry **tokens = NULL; Slapi_DN *base_sdn = NULL; Slapi_Backend *be = NULL; char *user_dn = NULL; char *filter = NULL; int pwd_numvals = 0; bool ret = false; int result = 0; int hint = 0; int i = 0; search_pb = slapi_pblock_new(); /* Fetch the user DN. */ user_dn = slapi_entry_get_ndn(bind_entry); if (user_dn == NULL) { slapi_log_error(SLAPI_LOG_FATAL, IPAPWD_PLUGIN_NAME, "ipapwd_do_otp_auth: error retrieving bind DN.\n"); goto done; } /* Search for TOTP tokens associated with this user. We search for * tokens who list this user as the owner in the same backend where * the user entry is located. */ filter = slapi_ch_smprintf("(&(%s=%s)(%s=%s))", SLAPI_ATTR_OBJECTCLASS, IPA_OTP_TOKEN_TOTP_OC, IPA_OTP_TOKEN_OWNER_TYPE, user_dn); be = slapi_be_select(slapi_entry_get_sdn(bind_entry)); if (be != NULL) { base_sdn = (Slapi_DN *) slapi_be_getsuffix(be, 0); } if (base_sdn == NULL) { slapi_log_error(SLAPI_LOG_FATAL, IPAPWD_PLUGIN_NAME, "ipapwd_do_otp_auth: error determining the search " "base for user \"%s\".\n", user_dn); } slapi_search_internal_set_pb(search_pb, slapi_sdn_get_ndn(base_sdn), LDAP_SCOPE_SUBTREE, filter, NULL, 0, NULL, NULL, ipapwd_plugin_id, 0); slapi_search_internal_pb(search_pb); slapi_pblock_get(search_pb, SLAPI_PLUGIN_INTOP_RESULT, &result); if (LDAP_SUCCESS != result) { slapi_log_error(SLAPI_LOG_FATAL, IPAPWD_PLUGIN_NAME, "ipapwd_do_otp_auth: error searching for tokens " "associated with user \"%s\" (err=%d).\n", user_dn, result); goto done; } slapi_pblock_get(search_pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &tokens); if (tokens == NULL) { /* This user has no associated tokens, so just bail out. */ goto done; } /* Fetch the userPassword values so we can perform the password checks * when processing tokens below. */ if (slapi_entry_attr_find(bind_entry, SLAPI_USERPWD_ATTR, &pwd_attr) != 0 || slapi_attr_get_numvalues(pwd_attr, &pwd_numvals) != 0) { slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME, "ipapwd_do_otp_auth: no passwords are set for user " "\"%s\".\n", user_dn); goto done; } /* We need to create a Slapi_Value array of the present password values * for the compare function. There's no nicer way of doing this. */ pwd_vals = (Slapi_Value **) slapi_ch_calloc(pwd_numvals, sizeof(Slapi_Value *)); for (hint = slapi_attr_first_value(pwd_attr, &pwd_vals[i]); hint != -1; hint = slapi_attr_next_value(pwd_attr, hint, &pwd_vals[i])) { ++i; } /* Loop through each token and attempt to authenticate. */ for (i = 0; tokens && tokens[i]; i++) { struct credentials credentials = {}; /* Parse the token entry and the credentials. */ if (!credentials_parse(tokens[i], creds, &credentials)) continue; /* Check if the password portion of the credential is correct. */ i = slapi_pw_find_sv(pwd_vals, credentials.ltp); if (i != 0) { slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME, "ipapwd_do_otp_auth: password check failed when " "processing token \"%s\" for user \"%s\".\n", slapi_entry_get_ndn(tokens[i]), user_dn); credentials_free_contents(&credentials); continue; } /* Attempt to perform OTP authentication for this token. */ if (!credentials.token.auth(&credentials.token, credentials.otp)) { slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME, "ipapwd_do_otp_auth: OTP auth failed when " "processing token \"%s\" for user \"%s\".\n", slapi_entry_get_ndn(tokens[i]), user_dn); credentials_free_contents(&credentials); continue; } /* Authentication successful! */ credentials_free_contents(&credentials); slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME, "ipapwd_do_otp_auth: successfully " "authenticated user \"%s\" using token " "\"%s\".\n", user_dn, slapi_entry_get_ndn(tokens[i])); ret = true; break; } done: slapi_ch_free_string(&filter); slapi_free_search_results_internal(search_pb); slapi_pblock_destroy(search_pb); return ret; }