freebsd-skq/crypto/heimdal/kcm/acquire.c
2008-05-07 13:39:42 +00:00

532 lines
13 KiB
C

/*
* 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;
}