/* * Copyright (c) 2005, PADL Software Pty Ltd. * 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. * * 3. Neither the name of PADL Software nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY PADL SOFTWARE 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 PADL SOFTWARE 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 "kcm_locl.h" RCSID("$Id: acquire.c 22118 2007-12-03 21:44:00Z lha $"); static krb5_error_code change_pw_and_update_keytab(krb5_context context, kcm_ccache ccache); /* * Get a new ticket using a keytab/cached key and swap it into * an existing redentials cache */ krb5_error_code kcm_ccache_acquire(krb5_context context, kcm_ccache ccache, krb5_creds **credp) { krb5_error_code ret = 0; krb5_creds cred; krb5_const_realm realm; krb5_get_init_creds_opt opt; krb5_ccache_data ccdata; char *in_tkt_service = NULL; int done = 0; memset(&cred, 0, sizeof(cred)); KCM_ASSERT_VALID(ccache); /* We need a cached key or keytab to acquire credentials */ if (ccache->flags & KCM_FLAGS_USE_CACHED_KEY) { if (ccache->key.keyblock.keyvalue.length == 0) krb5_abortx(context, "kcm_ccache_acquire: KCM_FLAGS_USE_CACHED_KEY without key"); } else if (ccache->flags & KCM_FLAGS_USE_KEYTAB) { if (ccache->key.keytab == NULL) krb5_abortx(context, "kcm_ccache_acquire: KCM_FLAGS_USE_KEYTAB without keytab"); } else { kcm_log(0, "Cannot acquire initial credentials for cache %s without key", ccache->name); return KRB5_FCC_INTERNAL; } HEIMDAL_MUTEX_lock(&ccache->mutex); /* Fake up an internal ccache */ kcm_internal_ccache(context, ccache, &ccdata); /* Now, actually acquire the creds */ if (ccache->server != NULL) { ret = krb5_unparse_name(context, ccache->server, &in_tkt_service); if (ret) { kcm_log(0, "Failed to unparse service principal name for cache %s: %s", ccache->name, krb5_get_err_text(context, ret)); return ret; } } realm = krb5_principal_get_realm(context, ccache->client); krb5_get_init_creds_opt_init(&opt); krb5_get_init_creds_opt_set_default_flags(context, "kcm", realm, &opt); if (ccache->tkt_life != 0) krb5_get_init_creds_opt_set_tkt_life(&opt, ccache->tkt_life); if (ccache->renew_life != 0) krb5_get_init_creds_opt_set_renew_life(&opt, ccache->renew_life); if (ccache->flags & KCM_FLAGS_USE_CACHED_KEY) { ret = krb5_get_init_creds_keyblock(context, &cred, ccache->client, &ccache->key.keyblock, 0, in_tkt_service, &opt); } else { /* loosely based on lib/krb5/init_creds_pw.c */ while (!done) { ret = krb5_get_init_creds_keytab(context, &cred, ccache->client, ccache->key.keytab, 0, in_tkt_service, &opt); switch (ret) { case KRB5KDC_ERR_KEY_EXPIRED: if (in_tkt_service != NULL && strcmp(in_tkt_service, "kadmin/changepw") == 0) { goto out; } ret = change_pw_and_update_keytab(context, ccache); if (ret) goto out; break; case 0: default: done = 1; break; } } } if (ret) { kcm_log(0, "Failed to acquire credentials for cache %s: %s", ccache->name, krb5_get_err_text(context, ret)); if (in_tkt_service != NULL) free(in_tkt_service); goto out; } if (in_tkt_service != NULL) free(in_tkt_service); /* Swap them in */ kcm_ccache_remove_creds_internal(context, ccache); ret = kcm_ccache_store_cred_internal(context, ccache, &cred, 0, credp); if (ret) { kcm_log(0, "Failed to store credentials for cache %s: %s", ccache->name, krb5_get_err_text(context, ret)); krb5_free_cred_contents(context, &cred); goto out; } out: HEIMDAL_MUTEX_unlock(&ccache->mutex); return ret; } static krb5_error_code change_pw(krb5_context context, kcm_ccache ccache, char *cpn, char *newpw) { krb5_error_code ret; krb5_creds cpw_cred; int result_code; krb5_data result_code_string; krb5_data result_string; krb5_get_init_creds_opt options; memset(&cpw_cred, 0, sizeof(cpw_cred)); krb5_get_init_creds_opt_init(&options); krb5_get_init_creds_opt_set_tkt_life(&options, 60); krb5_get_init_creds_opt_set_forwardable(&options, FALSE); krb5_get_init_creds_opt_set_proxiable(&options, FALSE); krb5_data_zero(&result_code_string); krb5_data_zero(&result_string); ret = krb5_get_init_creds_keytab(context, &cpw_cred, ccache->client, ccache->key.keytab, 0, "kadmin/changepw", &options); if (ret) { kcm_log(0, "Failed to acquire password change credentials " "for principal %s: %s", cpn, krb5_get_err_text(context, ret)); goto out; } ret = krb5_set_password(context, &cpw_cred, newpw, ccache->client, &result_code, &result_code_string, &result_string); if (ret) { kcm_log(0, "Failed to change password for principal %s: %s", cpn, krb5_get_err_text(context, ret)); goto out; } if (result_code) { kcm_log(0, "Failed to change password for principal %s: %.*s", cpn, (int)result_string.length, result_string.length > 0 ? (char *)result_string.data : ""); goto out; } out: krb5_data_free(&result_string); krb5_data_free(&result_code_string); krb5_free_cred_contents(context, &cpw_cred); return ret; } struct kcm_keyseed_data { krb5_salt salt; const char *password; }; static krb5_error_code kcm_password_key_proc(krb5_context context, krb5_enctype etype, krb5_salt salt, krb5_const_pointer keyseed, krb5_keyblock **key) { krb5_error_code ret; struct kcm_keyseed_data *s = (struct kcm_keyseed_data *)keyseed; /* we may be called multiple times */ krb5_free_salt(context, s->salt); krb5_data_zero(&s->salt.saltvalue); /* stash the salt */ s->salt.salttype = salt.salttype; ret = krb5_data_copy(&s->salt.saltvalue, salt.saltvalue.data, salt.saltvalue.length); if (ret) return ret; *key = (krb5_keyblock *)malloc(sizeof(**key)); if (*key == NULL) { return ENOMEM; } ret = krb5_string_to_key_salt(context, etype, s->password, s->salt, *key); if (ret) { free(*key); *key = NULL; } return ret; } static krb5_error_code get_salt_and_kvno(krb5_context context, kcm_ccache ccache, krb5_enctype *etypes, char *cpn, char *newpw, krb5_salt *salt, unsigned *kvno) { krb5_error_code ret; krb5_creds creds; krb5_ccache_data ccdata; krb5_flags options = 0; krb5_kdc_rep reply; struct kcm_keyseed_data s; memset(&creds, 0, sizeof(creds)); memset(&reply, 0, sizeof(reply)); s.password = NULL; s.salt.salttype = (int)ETYPE_NULL; krb5_data_zero(&s.salt.saltvalue); *kvno = 0; kcm_internal_ccache(context, ccache, &ccdata); s.password = newpw; /* Do an AS-REQ to determine salt and key version number */ ret = krb5_copy_principal(context, ccache->client, &creds.client); if (ret) return ret; /* Yes, get a ticket to ourselves */ ret = krb5_copy_principal(context, ccache->client, &creds.server); if (ret) { krb5_free_principal(context, creds.client); return ret; } ret = krb5_get_in_tkt(context, options, NULL, etypes, NULL, kcm_password_key_proc, &s, NULL, NULL, &creds, &ccdata, &reply); if (ret) { kcm_log(0, "Failed to get self ticket for principal %s: %s", cpn, krb5_get_err_text(context, ret)); krb5_free_salt(context, s.salt); } else { *salt = s.salt; /* retrieve stashed salt */ if (reply.kdc_rep.enc_part.kvno != NULL) *kvno = *(reply.kdc_rep.enc_part.kvno); } /* ccache may have been modified but it will get trashed anyway */ krb5_free_cred_contents(context, &creds); krb5_free_kdc_rep(context, &reply); return ret; } static krb5_error_code update_keytab_entry(krb5_context context, kcm_ccache ccache, krb5_enctype etype, char *cpn, char *spn, char *newpw, krb5_salt salt, unsigned kvno) { krb5_error_code ret; krb5_keytab_entry entry; krb5_data pw; memset(&entry, 0, sizeof(entry)); pw.data = (char *)newpw; pw.length = strlen(newpw); ret = krb5_string_to_key_data_salt(context, etype, pw, salt, &entry.keyblock); if (ret) { kcm_log(0, "String to key conversion failed for principal %s " "and etype %d: %s", cpn, etype, krb5_get_err_text(context, ret)); return ret; } if (spn == NULL) { ret = krb5_copy_principal(context, ccache->client, &entry.principal); if (ret) { kcm_log(0, "Failed to copy principal name %s: %s", cpn, krb5_get_err_text(context, ret)); return ret; } } else { ret = krb5_parse_name(context, spn, &entry.principal); if (ret) { kcm_log(0, "Failed to parse SPN alias %s: %s", spn, krb5_get_err_text(context, ret)); return ret; } } entry.vno = kvno; entry.timestamp = time(NULL); ret = krb5_kt_add_entry(context, ccache->key.keytab, &entry); if (ret) { kcm_log(0, "Failed to update keytab for principal %s " "and etype %d: %s", cpn, etype, krb5_get_err_text(context, ret)); } krb5_kt_free_entry(context, &entry); return ret; } static krb5_error_code update_keytab_entries(krb5_context context, kcm_ccache ccache, krb5_enctype *etypes, char *cpn, char *spn, char *newpw, krb5_salt salt, unsigned kvno) { krb5_error_code ret = 0; int i; for (i = 0; etypes[i] != ETYPE_NULL; i++) { ret = update_keytab_entry(context, ccache, etypes[i], cpn, spn, newpw, salt, kvno); if (ret) break; } return ret; } static void generate_random_pw(krb5_context context, char *buf, size_t bufsiz) { unsigned char x[512], *p; size_t i; memset(x, 0, sizeof(x)); krb5_generate_random_block(x, sizeof(x)); p = x; for (i = 0; i < bufsiz; i++) { while (isprint(*p) == 0) p++; if (p - x >= sizeof(x)) { krb5_generate_random_block(x, sizeof(x)); p = x; } buf[i] = (char)*p++; } buf[bufsiz - 1] = '\0'; memset(x, 0, sizeof(x)); } static krb5_error_code change_pw_and_update_keytab(krb5_context context, kcm_ccache ccache) { char newpw[121]; krb5_error_code ret; unsigned kvno; krb5_salt salt; krb5_enctype *etypes = NULL; int i; char *cpn = NULL; char **spns = NULL; krb5_data_zero(&salt.saltvalue); ret = krb5_unparse_name(context, ccache->client, &cpn); if (ret) { kcm_log(0, "Failed to unparse name: %s", krb5_get_err_text(context, ret)); goto out; } ret = krb5_get_default_in_tkt_etypes(context, &etypes); if (ret) { kcm_log(0, "Failed to determine default encryption types: %s", krb5_get_err_text(context, ret)); goto out; } /* Generate a random password (there is no set keys protocol) */ generate_random_pw(context, newpw, sizeof(newpw)); /* Change it */ ret = change_pw(context, ccache, cpn, newpw); if (ret) goto out; /* Do an AS-REQ to determine salt and key version number */ ret = get_salt_and_kvno(context, ccache, etypes, cpn, newpw, &salt, &kvno); if (ret) { kcm_log(0, "Failed to determine salting principal for principal %s: %s", cpn, krb5_get_err_text(context, ret)); goto out; } /* Add canonical name */ ret = update_keytab_entries(context, ccache, etypes, cpn, NULL, newpw, salt, kvno); if (ret) goto out; /* Add SPN aliases, if any */ spns = krb5_config_get_strings(context, NULL, "kcm", "system_ccache", "spn_aliases", NULL); if (spns != NULL) { for (i = 0; spns[i] != NULL; i++) { ret = update_keytab_entries(context, ccache, etypes, cpn, spns[i], newpw, salt, kvno); if (ret) goto out; } } kcm_log(0, "Changed expired password for principal %s in cache %s", cpn, ccache->name); out: if (cpn != NULL) free(cpn); if (spns != NULL) krb5_config_free_strings(spns); if (etypes != NULL) free(etypes); krb5_free_salt(context, salt); memset(newpw, 0, sizeof(newpw)); return ret; }