xref: /freebsd/crypto/heimdal/kcm/acquire.c (revision aa0a1e58)
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 
35 RCSID("$Id: acquire.c 22118 2007-12-03 21:44:00Z lha $");
36 
37 static krb5_error_code
38 change_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 
45 krb5_error_code
46 kcm_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 
160 out:
161     HEIMDAL_MUTEX_unlock(&ccache->mutex);
162 
163     return ret;
164 }
165 
166 static krb5_error_code
167 change_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 
224 out:
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 
232 struct kcm_keyseed_data {
233     krb5_salt salt;
234     const char *password;
235 };
236 
237 static krb5_error_code
238 kcm_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 
275 static krb5_error_code
276 get_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 
343 static krb5_error_code
344 update_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 
403 static krb5_error_code
404 update_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 
426 static void
427 generate_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 
452 static krb5_error_code
453 change_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 
519 out:
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