1/*
2 * Copyright (c) 2005, PADL Software Pty Ltd.
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 *    notice, this list of conditions and the following disclaimer.
11 *
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.
15 *
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.
19 *
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.
31 */
32
33#include "kcm_locl.h"
34
35RCSID("$Id: acquire.c 22118 2007-12-03 21:44:00Z lha $");
36
37static krb5_error_code
38change_pw_and_update_keytab(krb5_context context, kcm_ccache ccache);
39
40/*
41 * Get a new ticket using a keytab/cached key and swap it into
42 * an existing redentials cache
43 */
44
45krb5_error_code
46kcm_ccache_acquire(krb5_context context,
47		   kcm_ccache ccache,
48		   krb5_creds **credp)
49{
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;
57
58    memset(&cred, 0, sizeof(cred));
59
60    KCM_ASSERT_VALID(ccache);
61
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;
75    }
76
77    HEIMDAL_MUTEX_lock(&ccache->mutex);
78
79    /* Fake up an internal ccache */
80    kcm_internal_ccache(context, ccache, &ccdata);
81
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;
89	}
90    }
91
92    realm = krb5_principal_get_realm(context, ccache->client);
93
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);
100
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,
106					   0,
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,
116					     0,
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;
124		}
125
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;
134	    }
135	}
136    }
137
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;
144    }
145
146    if (in_tkt_service != NULL)
147	free(in_tkt_service);
148
149    /* Swap them in */
150    kcm_ccache_remove_creds_internal(context, ccache);
151
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_cred_contents(context, &cred);
157	goto out;
158    }
159
160out:
161    HEIMDAL_MUTEX_unlock(&ccache->mutex);
162
163    return ret;
164}
165
166static krb5_error_code
167change_pw(krb5_context context,
168	  kcm_ccache ccache,
169	  char *cpn,
170	  char *newpw)
171{
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;
178
179    memset(&cpw_cred, 0, sizeof(cpw_cred));
180
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);
185
186    krb5_data_zero(&result_code_string);
187    krb5_data_zero(&result_string);
188
189    ret = krb5_get_init_creds_keytab(context,
190				     &cpw_cred,
191				     ccache->client,
192				     ccache->key.keytab,
193				     0,
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;
201    }
202
203    ret = krb5_set_password(context,
204			    &cpw_cred,
205			    newpw,
206			    ccache->client,
207			    &result_code,
208			    &result_code_string,
209			    &result_string);
210    if (ret) {
211	kcm_log(0, "Failed to change password for principal %s: %s",
212		cpn, krb5_get_err_text(context, ret));
213	goto out;
214    }
215
216    if (result_code) {
217	kcm_log(0, "Failed to change password for principal %s: %.*s",
218		cpn,
219		(int)result_string.length,
220		result_string.length > 0 ? (char *)result_string.data : "");
221	goto out;
222    }
223
224out:
225    krb5_data_free(&result_string);
226    krb5_data_free(&result_code_string);
227    krb5_free_cred_contents(context, &cpw_cred);
228
229    return ret;
230}
231
232struct kcm_keyseed_data {
233    krb5_salt salt;
234    const char *password;
235};
236
237static krb5_error_code
238kcm_password_key_proc(krb5_context context,
239		      krb5_enctype etype,
240		      krb5_salt salt,
241		      krb5_const_pointer keyseed,
242		      krb5_keyblock **key)
243{
244    krb5_error_code ret;
245    struct kcm_keyseed_data *s = (struct kcm_keyseed_data *)keyseed;
246
247    /* we may be called multiple times */
248    krb5_free_salt(context, s->salt);
249    krb5_data_zero(&s->salt.saltvalue);
250
251    /* stash the salt */
252    s->salt.salttype = salt.salttype;
253
254    ret = krb5_data_copy(&s->salt.saltvalue,
255		         salt.saltvalue.data,
256			 salt.saltvalue.length);
257    if (ret)
258	return ret;
259
260    *key = (krb5_keyblock *)malloc(sizeof(**key));
261    if (*key == NULL) {
262	return ENOMEM;
263    }
264
265    ret = krb5_string_to_key_salt(context, etype, s->password,
266				  s->salt, *key);
267    if (ret) {
268	free(*key);
269	*key = NULL;
270    }
271
272    return ret;
273}
274
275static krb5_error_code
276get_salt_and_kvno(krb5_context context,
277		  kcm_ccache ccache,
278		  krb5_enctype *etypes,
279		  char *cpn,
280		  char *newpw,
281		  krb5_salt *salt,
282		  unsigned *kvno)
283{
284    krb5_error_code ret;
285    krb5_creds creds;
286    krb5_ccache_data ccdata;
287    krb5_flags options = 0;
288    krb5_kdc_rep reply;
289    struct kcm_keyseed_data s;
290
291    memset(&creds, 0, sizeof(creds));
292    memset(&reply, 0, sizeof(reply));
293
294    s.password = NULL;
295    s.salt.salttype = (int)ETYPE_NULL;
296    krb5_data_zero(&s.salt.saltvalue);
297
298    *kvno = 0;
299    kcm_internal_ccache(context, ccache, &ccdata);
300    s.password = newpw;
301
302    /* Do an AS-REQ to determine salt and key version number */
303    ret = krb5_copy_principal(context, ccache->client, &creds.client);
304    if (ret)
305	return ret;
306
307    /* Yes, get a ticket to ourselves */
308    ret = krb5_copy_principal(context, ccache->client, &creds.server);
309    if (ret) {
310	krb5_free_principal(context, creds.client);
311	return ret;
312    }
313
314    ret = krb5_get_in_tkt(context,
315			  options,
316			  NULL,
317			  etypes,
318			  NULL,
319			  kcm_password_key_proc,
320			  &s,
321			  NULL,
322			  NULL,
323			  &creds,
324			  &ccdata,
325			  &reply);
326    if (ret) {
327	kcm_log(0, "Failed to get self ticket for principal %s: %s",
328		cpn, krb5_get_err_text(context, ret));
329	krb5_free_salt(context, s.salt);
330    } else {
331	*salt = s.salt; /* retrieve stashed salt */
332	if (reply.kdc_rep.enc_part.kvno != NULL)
333	    *kvno = *(reply.kdc_rep.enc_part.kvno);
334    }
335    /* ccache may have been modified but it will get trashed anyway */
336
337    krb5_free_cred_contents(context, &creds);
338    krb5_free_kdc_rep(context, &reply);
339
340    return ret;
341}
342
343static krb5_error_code
344update_keytab_entry(krb5_context context,
345		    kcm_ccache ccache,
346		    krb5_enctype etype,
347		    char *cpn,
348		    char *spn,
349		    char *newpw,
350		    krb5_salt salt,
351		    unsigned kvno)
352{
353    krb5_error_code ret;
354    krb5_keytab_entry entry;
355    krb5_data pw;
356
357    memset(&entry, 0, sizeof(entry));
358
359    pw.data = (char *)newpw;
360    pw.length = strlen(newpw);
361
362    ret = krb5_string_to_key_data_salt(context, etype, pw,
363				       salt, &entry.keyblock);
364    if (ret) {
365	kcm_log(0, "String to key conversion failed for principal %s "
366		"and etype %d: %s",
367		cpn, etype, krb5_get_err_text(context, ret));
368	return ret;
369    }
370
371    if (spn == NULL) {
372	ret = krb5_copy_principal(context, ccache->client,
373				  &entry.principal);
374	if (ret) {
375	    kcm_log(0, "Failed to copy principal name %s: %s",
376		    cpn, krb5_get_err_text(context, ret));
377	    return ret;
378	}
379    } else {
380	ret = krb5_parse_name(context, spn, &entry.principal);
381	if (ret) {
382	    kcm_log(0, "Failed to parse SPN alias %s: %s",
383		    spn, krb5_get_err_text(context, ret));
384	    return ret;
385	}
386    }
387
388    entry.vno = kvno;
389    entry.timestamp = time(NULL);
390
391    ret = krb5_kt_add_entry(context, ccache->key.keytab, &entry);
392    if (ret) {
393	kcm_log(0, "Failed to update keytab for principal %s "
394		"and etype %d: %s",
395		cpn, etype, krb5_get_err_text(context, ret));
396    }
397
398    krb5_kt_free_entry(context, &entry);
399
400    return ret;
401}
402
403static krb5_error_code
404update_keytab_entries(krb5_context context,
405		      kcm_ccache ccache,
406		      krb5_enctype *etypes,
407		      char *cpn,
408		      char *spn,
409		      char *newpw,
410		      krb5_salt salt,
411		      unsigned kvno)
412{
413    krb5_error_code ret = 0;
414    int i;
415
416    for (i = 0; etypes[i] != ETYPE_NULL; i++) {
417	ret = update_keytab_entry(context, ccache, etypes[i],
418				  cpn, spn, newpw, salt, kvno);
419	if (ret)
420	    break;
421    }
422
423    return ret;
424}
425
426static void
427generate_random_pw(krb5_context context,
428		   char *buf,
429		   size_t bufsiz)
430{
431    unsigned char x[512], *p;
432    size_t i;
433
434    memset(x, 0, sizeof(x));
435    krb5_generate_random_block(x, sizeof(x));
436    p = x;
437
438    for (i = 0; i < bufsiz; i++) {
439	while (isprint(*p) == 0)
440	    p++;
441
442	if (p - x >= sizeof(x)) {
443	    krb5_generate_random_block(x, sizeof(x));
444	    p = x;
445	}
446	buf[i] = (char)*p++;
447    }
448    buf[bufsiz - 1] = '\0';
449    memset(x, 0, sizeof(x));
450}
451
452static krb5_error_code
453change_pw_and_update_keytab(krb5_context context,
454			    kcm_ccache ccache)
455{
456    char newpw[121];
457    krb5_error_code ret;
458    unsigned kvno;
459    krb5_salt salt;
460    krb5_enctype *etypes = NULL;
461    int i;
462    char *cpn = NULL;
463    char **spns = NULL;
464
465    krb5_data_zero(&salt.saltvalue);
466
467    ret = krb5_unparse_name(context, ccache->client, &cpn);
468    if (ret) {
469	kcm_log(0, "Failed to unparse name: %s",
470		krb5_get_err_text(context, ret));
471	goto out;
472    }
473
474    ret = krb5_get_default_in_tkt_etypes(context, &etypes);
475    if (ret) {
476	kcm_log(0, "Failed to determine default encryption types: %s",
477		krb5_get_err_text(context, ret));
478	goto out;
479    }
480
481    /* Generate a random password (there is no set keys protocol) */
482    generate_random_pw(context, newpw, sizeof(newpw));
483
484    /* Change it */
485    ret = change_pw(context, ccache, cpn, newpw);
486    if (ret)
487	goto out;
488
489    /* Do an AS-REQ to determine salt and key version number */
490    ret = get_salt_and_kvno(context, ccache, etypes, cpn, newpw,
491			    &salt, &kvno);
492    if (ret) {
493	kcm_log(0, "Failed to determine salting principal for principal %s: %s",
494		cpn, krb5_get_err_text(context, ret));
495	goto out;
496    }
497
498    /* Add canonical name */
499    ret = update_keytab_entries(context, ccache, etypes, cpn,
500				NULL, newpw, salt, kvno);
501    if (ret)
502	goto out;
503
504    /* Add SPN aliases, if any */
505    spns = krb5_config_get_strings(context, NULL, "kcm",
506				   "system_ccache", "spn_aliases", NULL);
507    if (spns != NULL) {
508	for (i = 0; spns[i] != NULL; i++) {
509	    ret = update_keytab_entries(context, ccache, etypes, cpn,
510					spns[i], newpw, salt, kvno);
511	    if (ret)
512		goto out;
513	}
514    }
515
516    kcm_log(0, "Changed expired password for principal %s in cache %s",
517	    cpn, ccache->name);
518
519out:
520    if (cpn != NULL)
521	free(cpn);
522    if (spns != NULL)
523	krb5_config_free_strings(spns);
524    if (etypes != NULL)
525	free(etypes);
526    krb5_free_salt(context, salt);
527    memset(newpw, 0, sizeof(newpw));
528
529    return ret;
530}
531
532