Allow system credentials cache to be configured in krb5.conf
[heimdal.git] / kcm / acquire.c
blob21c8657ba73560e350812747c6ae7cf2af0e0575
1 /*
2 * Copyright (c) 2005, PADL Software Pty Ltd.
3 * All rights reserved.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
16 * 3. Neither the name of PADL Software nor the names of its contributors
17 * may be used to endorse or promote products derived from this software
18 * without specific prior written permission.
20 * THIS SOFTWARE IS PROVIDED BY PADL SOFTWARE AND CONTRIBUTORS ``AS IS'' AND
21 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23 * ARE DISCLAIMED. IN NO EVENT SHALL PADL SOFTWARE OR CONTRIBUTORS BE LIABLE
24 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
26 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
27 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
29 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
30 * SUCH DAMAGE.
33 #include "kcm_locl.h"
35 RCSID("$Id$");
37 static krb5_error_code
38 change_pw_and_update_keytab(krb5_context context, kcm_ccache ccache);
41 * Get a new ticket using a keytab/cached key and swap it into
42 * an existing redentials cache
45 krb5_error_code
46 kcm_ccache_acquire(krb5_context context,
47 kcm_ccache ccache,
48 krb5_creds **credp)
50 krb5_error_code ret = 0;
51 krb5_creds cred;
52 krb5_const_realm realm;
53 krb5_get_init_creds_opt opt;
54 krb5_ccache_data ccdata;
55 char *in_tkt_service = NULL;
56 int done = 0;
58 memset(&cred, 0, sizeof(cred));
60 KCM_ASSERT_VALID(ccache);
62 /* We need a cached key or keytab to acquire credentials */
63 if (ccache->flags & KCM_FLAGS_USE_CACHED_KEY) {
64 if (ccache->key.keyblock.keyvalue.length == 0)
65 krb5_abortx(context,
66 "kcm_ccache_acquire: KCM_FLAGS_USE_CACHED_KEY without key");
67 } else if (ccache->flags & KCM_FLAGS_USE_KEYTAB) {
68 if (ccache->key.keytab == NULL)
69 krb5_abortx(context,
70 "kcm_ccache_acquire: KCM_FLAGS_USE_KEYTAB without keytab");
71 } else {
72 kcm_log(0, "Cannot acquire initial credentials for cache %s without key",
73 ccache->name);
74 return KRB5_FCC_INTERNAL;
77 HEIMDAL_MUTEX_lock(&ccache->mutex);
79 /* Fake up an internal ccache */
80 kcm_internal_ccache(context, ccache, &ccdata);
82 /* Now, actually acquire the creds */
83 if (ccache->server != NULL) {
84 ret = krb5_unparse_name(context, ccache->server, &in_tkt_service);
85 if (ret) {
86 kcm_log(0, "Failed to unparse service principal name for cache %s: %s",
87 ccache->name, krb5_get_err_text(context, ret));
88 return ret;
92 realm = krb5_principal_get_realm(context, ccache->client);
94 krb5_get_init_creds_opt_init(&opt);
95 krb5_get_init_creds_opt_set_default_flags(context, "kcm", realm, &opt);
96 if (ccache->tkt_life != 0)
97 krb5_get_init_creds_opt_set_tkt_life(&opt, ccache->tkt_life);
98 if (ccache->renew_life != 0)
99 krb5_get_init_creds_opt_set_renew_life(&opt, ccache->renew_life);
101 if (ccache->flags & KCM_FLAGS_USE_CACHED_KEY) {
102 ret = krb5_get_init_creds_keyblock(context,
103 &cred,
104 ccache->client,
105 &ccache->key.keyblock,
107 in_tkt_service,
108 &opt);
109 } else {
110 /* loosely based on lib/krb5/init_creds_pw.c */
111 while (!done) {
112 ret = krb5_get_init_creds_keytab(context,
113 &cred,
114 ccache->client,
115 ccache->key.keytab,
117 in_tkt_service,
118 &opt);
119 switch (ret) {
120 case KRB5KDC_ERR_KEY_EXPIRED:
121 if (in_tkt_service != NULL &&
122 strcmp(in_tkt_service, "kadmin/changepw") == 0) {
123 goto out;
126 ret = change_pw_and_update_keytab(context, ccache);
127 if (ret)
128 goto out;
129 break;
130 case 0:
131 default:
132 done = 1;
133 break;
138 if (ret) {
139 kcm_log(0, "Failed to acquire credentials for cache %s: %s",
140 ccache->name, krb5_get_err_text(context, ret));
141 if (in_tkt_service != NULL)
142 free(in_tkt_service);
143 goto out;
146 if (in_tkt_service != NULL)
147 free(in_tkt_service);
149 /* Swap them in */
150 kcm_ccache_remove_creds_internal(context, ccache);
152 ret = kcm_ccache_store_cred_internal(context, ccache, &cred, 0, credp);
153 if (ret) {
154 kcm_log(0, "Failed to store credentials for cache %s: %s",
155 ccache->name, krb5_get_err_text(context, ret));
156 krb5_free_creds_contents(context, &cred);
157 goto out;
160 out:
161 HEIMDAL_MUTEX_unlock(&ccache->mutex);
163 return ret;
166 static krb5_error_code
167 change_pw(krb5_context context,
168 kcm_ccache ccache,
169 char *cpn,
170 char *newpw)
172 krb5_error_code ret;
173 krb5_creds cpw_cred;
174 int result_code;
175 krb5_data result_code_string;
176 krb5_data result_string;
177 krb5_get_init_creds_opt options;
179 memset(&cpw_cred, 0, sizeof(cpw_cred));
181 krb5_get_init_creds_opt_init(&options);
182 krb5_get_init_creds_opt_set_tkt_life(&options, 60);
183 krb5_get_init_creds_opt_set_forwardable(&options, FALSE);
184 krb5_get_init_creds_opt_set_proxiable(&options, FALSE);
186 krb5_data_zero(&result_code_string);
187 krb5_data_zero(&result_string);
189 ret = krb5_get_init_creds_keytab(context,
190 &cpw_cred,
191 ccache->client,
192 ccache->key.keytab,
194 "kadmin/changepw",
195 &options);
196 if (ret) {
197 kcm_log(0, "Failed to acquire password change credentials "
198 "for principal %s: %s",
199 cpn, krb5_get_err_text(context, ret));
200 goto out;
203 ret = krb5_change_password(context,
204 &cpw_cred,
205 newpw,
206 &result_code,
207 &result_code_string,
208 &result_string);
209 if (ret) {
210 kcm_log(0, "Failed to change password for principal %s: %s",
211 cpn, krb5_get_err_text(context, ret));
212 goto out;
215 if (result_code) {
216 kcm_log(0, "Failed to change password for principal %s: %.*s",
217 cpn,
218 (int)result_string.length,
219 result_string.length > 0 ? (char *)result_string.data : "");
220 goto out;
223 out:
224 krb5_data_free(&result_string);
225 krb5_data_free(&result_code_string);
226 krb5_free_cred_contents(context, &cpw_cred);
228 return ret;
231 struct kcm_keyseed_data {
232 krb5_salt salt;
233 const char *password;
236 static krb5_error_code
237 kcm_password_key_proc(krb5_context context,
238 krb5_enctype etype,
239 krb5_salt salt,
240 krb5_const_pointer keyseed,
241 krb5_keyblock **key)
243 krb5_error_code ret;
244 struct kcm_keyseed_data *s = (struct kcm_keyseed_data *)keyseed;
246 /* we may be called multiple times */
247 krb5_free_salt(context, s->salt);
248 krb5_data_zero(&s->salt.saltvalue);
250 /* stash the salt */
251 s->salt.salttype = salt.salttype;
253 ret = krb5_data_copy(&s->salt.saltvalue,
254 salt.saltvalue.data,
255 salt.saltvalue.length);
256 if (ret)
257 return ret;
259 *key = (krb5_keyblock *)malloc(sizeof(**key));
260 if (*key == NULL) {
261 return ENOMEM;
264 ret = krb5_string_to_key_salt(context, etype, s->password,
265 s->salt, *key);
266 if (ret) {
267 free(*key);
268 *key = NULL;
271 return ret;
274 static krb5_error_code
275 get_salt_and_kvno(krb5_context context,
276 kcm_ccache ccache,
277 krb5_enctype *etypes,
278 char *cpn,
279 char *newpw,
280 krb5_salt *salt,
281 unsigned *kvno)
283 krb5_error_code ret;
284 krb5_creds creds;
285 krb5_ccache_data ccdata;
286 krb5_flags options = 0;
287 krb5_kdc_rep reply;
288 struct kcm_keyseed_data s;
290 memset(&creds, 0, sizeof(creds));
291 memset(&reply, 0, sizeof(reply));
293 s.password = NULL;
294 s.salt.salttype = ETYPE_NULL;
295 krb5_data_zero(&s.salt.saltvalue);
297 *kvno = 0;
298 kcm_internal_ccache(context, ccache, &ccdata);
299 s.password = newpw;
301 /* Do an AS-REQ to determine salt and key version number */
302 ret = krb5_copy_principal(context, ccache->client, &creds.client);
303 if (ret)
304 return ret;
306 /* Yes, get a ticket to ourselves */
307 ret = krb5_copy_principal(context, ccache->client, &creds.server);
308 if (ret) {
309 krb5_free_principal(context, creds.client);
310 return ret;
313 ret = krb5_get_in_tkt(context,
314 options,
315 NULL,
316 etypes,
317 NULL,
318 kcm_password_key_proc,
320 NULL,
321 NULL,
322 &creds,
323 &ccdata,
324 &reply);
325 if (ret) {
326 kcm_log(0, "Failed to get self ticket for principal %s: %s",
327 cpn, krb5_get_err_text(context, ret));
328 krb5_free_salt(context, s.salt);
329 } else {
330 *salt = s.salt; /* retrieve stashed salt */
331 if (reply.kdc_rep.enc_part.kvno != NULL)
332 *kvno = *(reply.kdc_rep.enc_part.kvno);
334 /* ccache may have been modified but it will get trashed anyway */
336 krb5_free_creds_contents(context, &creds);
337 krb5_free_kdc_rep(context, &reply);
339 return ret;
342 static krb5_error_code
343 update_keytab_entry(krb5_context context,
344 kcm_ccache ccache,
345 krb5_enctype etype,
346 char *cpn,
347 char *spn,
348 char *newpw,
349 krb5_salt salt,
350 unsigned kvno)
352 krb5_error_code ret;
353 krb5_keytab_entry entry;
354 krb5_data pw;
356 memset(&entry, 0, sizeof(entry));
358 pw.data = (char *)newpw;
359 pw.length = strlen(newpw);
361 ret = krb5_string_to_key_data_salt(context, etype, pw,
362 salt, &entry.keyblock);
363 if (ret) {
364 kcm_log(0, "String to key conversion failed for principal %s "
365 "and etype %d: %s",
366 cpn, etype, krb5_get_err_text(context, ret));
367 return ret;
370 if (spn == NULL) {
371 ret = krb5_copy_principal(context, ccache->client,
372 &entry.principal);
373 if (ret) {
374 kcm_log(0, "Failed to copy principal name %s: %s",
375 cpn, krb5_get_err_text(context, ret));
376 return ret;
378 } else {
379 ret = krb5_parse_name(context, spn, &entry.principal);
380 if (ret) {
381 kcm_log(0, "Failed to parse SPN alias %s: %s",
382 spn, krb5_get_err_text(context, ret));
383 return ret;
387 entry.vno = kvno;
388 entry.timestamp = time(NULL);
390 ret = krb5_kt_add_entry(context, ccache->key.keytab, &entry);
391 if (ret) {
392 kcm_log(0, "Failed to update keytab for principal %s "
393 "and etype %d: %s",
394 cpn, etype, krb5_get_err_text(context, ret));
397 krb5_kt_free_entry(context, &entry);
399 return ret;
402 static krb5_error_code
403 update_keytab_entries(krb5_context context,
404 kcm_ccache ccache,
405 krb5_enctype *etypes,
406 char *cpn,
407 char *spn,
408 char *newpw,
409 krb5_salt salt,
410 unsigned kvno)
412 krb5_error_code ret = 0;
413 int i;
415 for (i = 0; etypes[i] != ETYPE_NULL; i++) {
416 ret = update_keytab_entry(context, ccache, etypes[i],
417 cpn, spn, newpw, salt, kvno);
418 if (ret)
419 break;
422 return ret;
425 static void
426 generate_random_pw(krb5_context context,
427 unsigned char *buf,
428 size_t bufsiz)
430 unsigned char x[512], *p;
431 size_t i;
433 memset(x, 0, sizeof(x));
434 krb5_generate_random_block(x, sizeof(x));
435 p = x;
437 for (i = 0; i < bufsiz; i++) {
438 while (isprint(*p) == 0)
439 p++;
441 if (p - x >= sizeof(x)) {
442 krb5_generate_random_block(x, sizeof(x));
443 p = x;
445 buf[i] = *p++;
447 buf[bufsiz - 1] = '\0';
448 memset(x, 0, sizeof(x));
451 static krb5_error_code
452 change_pw_and_update_keytab(krb5_context context,
453 kcm_ccache ccache)
455 char newpw[121];
456 krb5_error_code ret;
457 unsigned kvno;
458 krb5_salt salt;
459 krb5_enctype *etypes = NULL;
460 int i;
461 char *cpn = NULL;
462 char **spns = NULL;
464 krb5_data_zero(&salt.saltvalue);
466 ret = krb5_unparse_name(context, ccache->client, &cpn);
467 if (ret) {
468 kcm_log(0, "Failed to unparse name: %s",
469 krb5_get_err_text(context, ret));
470 goto out;
473 ret = krb5_get_default_in_tkt_etypes(context, &etypes);
474 if (ret) {
475 kcm_log(0, "Failed to determine default encryption types: %s",
476 krb5_get_err_text(context, ret));
477 goto out;
480 /* Generate a random password (there is no set keys protocol) */
481 generate_random_pw(context, newpw, sizeof(newpw));
483 /* Change it */
484 ret = change_pw(context, ccache, cpn, newpw);
485 if (ret)
486 goto out;
488 /* Do an AS-REQ to determine salt and key version number */
489 ret = get_salt_and_kvno(context, ccache, etypes, cpn, newpw,
490 &salt, &kvno);
491 if (ret) {
492 kcm_log(0, "Failed to determine salting principal for principal %s: %s",
493 cpn, krb5_get_err_text(context, ret));
494 goto out;
497 /* Add canonical name */
498 ret = update_keytab_entries(context, ccache, etypes, cpn,
499 NULL, newpw, salt, kvno);
500 if (ret)
501 goto out;
503 /* Add SPN aliases, if any */
504 spns = krb5_config_get_strings(context, NULL, "kcm",
505 "system_ccache", "spn_aliases", NULL);
506 if (spns != NULL) {
507 for (i = 0; spns[i] != NULL; i++) {
508 ret = update_keytab_entries(context, ccache, etypes, cpn,
509 spns[i], newpw, salt, kvno);
510 if (ret)
511 goto out;
515 kcm_log(0, "Changed expired password for principal %s in cache %s",
516 cpn, ccache->name);
518 out:
519 if (cpn != NULL)
520 free(cpn);
521 if (spns != NULL)
522 krb5_config_free_strings(spns);
523 if (etypes != NULL)
524 free(etypes);
525 krb5_free_salt(context, salt);
526 memset(newpw, 0, sizeof(newpw));
528 return ret;