xref: /illumos-gate/usr/src/lib/krb5/kadm5/srv/chgpwd.c (revision b07ce584f4e28873b8927d7f83d9d3275a0f3ed2)
1 /*
2  * Copyright 2007 Sun Microsystems, Inc.  All rights reserved.
3  * Use is subject to license terms.
4  */
5 
6 #pragma ident	"%Z%%M%	%I%	%E% SMI"
7 
8 /*
9  * lib/krb5/kadm5/srv/chgpwd.c
10  *
11  * Copyright 1998 by the Massachusetts Institute of Technology.
12  * All Rights Reserved.
13  *
14  * Export of this software from the United States of America may
15  *   require a specific license from the United States Government.
16  *   It is the responsibility of any person or organization contemplating
17  *   export to obtain such a license before exporting.
18  *
19  * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
20  * distribute this software and its documentation for any purpose and
21  * without fee is hereby granted, provided that the above copyright
22  * notice appear in all copies and that both that copyright notice and
23  * this permission notice appear in supporting documentation, and that
24  * the name of M.I.T. not be used in advertising or publicity pertaining
25  * to distribution of the software without specific, written prior
26  * permission.  Furthermore if you modify this software you must label
27  * your software as modified software and not distribute it in such a
28  * fashion that it might be confused with the original M.I.T. software.
29  * M.I.T. makes no representations about the suitability of
30  * this software for any purpose.  It is provided "as is" without express
31  * or implied warranty.
32  *
33  */
34 
35 /*
36  * chgpwd.c - Handles changepw requests issued from non-Solaris krb5 clients.
37  */
38 
39 #include <libintl.h>
40 #include <locale.h>
41 #include <kadm5/admin.h>
42 #include <syslog.h>
43 #include <krb5/adm_proto.h>
44 
45 #define	MAXAPREQ 1500
46 
47 static krb5_error_code
48 process_chpw_request(krb5_context context, void *server_handle,
49 			char *realm, int s, krb5_keytab keytab,
50 			struct sockaddr_in *sin, krb5_data *req,
51 			krb5_data *rep)
52 {
53 	krb5_error_code ret;
54 	char *ptr;
55 	int plen, vno;
56 	krb5_address local_kaddr, remote_kaddr;
57 	int allocated_mem = 0;
58 	krb5_data ap_req, ap_rep;
59 	krb5_auth_context auth_context;
60 	krb5_principal changepw;
61 	krb5_ticket *ticket;
62 	krb5_data cipher, clear;
63 	struct sockaddr local_addr, remote_addr;
64 	int addrlen;
65 	krb5_replay_data replay;
66 	krb5_error krberror;
67 	int numresult;
68 	char strresult[1024];
69 	char *clientstr;
70 	size_t clen;
71 	char *cdots;
72 
73 	ret = 0;
74 	rep->length = 0;
75 
76 	auth_context = NULL;
77 	changepw = NULL;
78 	ap_rep.length = 0;
79 	ap_rep.data = NULL;
80 	ticket = NULL;
81 	clear.length = 0;
82 	clear.data = NULL;
83 	cipher.length = 0;
84 	cipher.data = NULL;
85 
86 	if (req->length < 4) {
87 		/*
88 		 * either this, or the server is printing bad messages,
89 		 * or the caller passed in garbage
90 		 */
91 		ret = KRB5KRB_AP_ERR_MODIFIED;
92 		numresult = KRB5_KPASSWD_MALFORMED;
93 		(void) strlcpy(strresult, "Request was truncated",
94 				sizeof (strresult));
95 		goto chpwfail;
96 	}
97 
98 	ptr = req->data;
99 
100 	/*
101 	 * Verify length
102 	 */
103 	plen = (*ptr++ & 0xff);
104 	plen = (plen<<8) | (*ptr++ & 0xff);
105 
106 	if (plen != req->length)
107 		return (KRB5KRB_AP_ERR_MODIFIED);
108 
109 	/*
110 	 * Verify version number
111 	 */
112 	vno = (*ptr++ & 0xff);
113 	vno = (vno<<8) | (*ptr++ & 0xff);
114 
115 	if (vno != 1) {
116 		ret = KRB5KDC_ERR_BAD_PVNO;
117 		numresult = KRB5_KPASSWD_MALFORMED;
118 		(void) snprintf(strresult, sizeof (strresult),
119 		    "Request contained unknown protocol version number %d",
120 		    vno);
121 		goto chpwfail;
122 	}
123 
124 	/*
125 	 * Read, check ap-req length
126 	 */
127 	ap_req.length = (*ptr++ & 0xff);
128 	ap_req.length = (ap_req.length<<8) | (*ptr++ & 0xff);
129 
130 	if (ptr + ap_req.length >= req->data + req->length) {
131 		ret = KRB5KRB_AP_ERR_MODIFIED;
132 		numresult = KRB5_KPASSWD_MALFORMED;
133 		(void) strlcpy(strresult, "Request was truncated in AP-REQ",
134 					sizeof (strresult));
135 		goto chpwfail;
136 	}
137 
138 	/*
139 	 * Verify ap_req
140 	 */
141 	ap_req.data = ptr;
142 	ptr += ap_req.length;
143 
144 	if (ret = krb5_auth_con_init(context, &auth_context)) {
145 		krb5_klog_syslog(LOG_ERR,
146 				gettext("Change password request failed. "
147 					"Failed initializing auth context: %s"),
148 				error_message(ret));
149 		numresult = KRB5_KPASSWD_HARDERROR;
150 		(void) strlcpy(strresult, "Failed initializing auth context",
151 					sizeof (strresult));
152 		goto chpwfail;
153 	}
154 
155 	if (ret = krb5_auth_con_setflags(context, auth_context,
156 					KRB5_AUTH_CONTEXT_DO_SEQUENCE)) {
157 		krb5_klog_syslog(LOG_ERR,
158 				gettext("Change password request failed. "
159 						"Failed setting auth "
160 					    "context flags: %s"),
161 				error_message(ret));
162 		numresult = KRB5_KPASSWD_HARDERROR;
163 		(void) strlcpy(strresult, "Failed initializing auth context",
164 					sizeof (strresult));
165 		goto chpwfail;
166 	}
167 
168 	if (ret = krb5_build_principal(context, &changepw, strlen(realm), realm,
169 				    "kadmin", "changepw", NULL)) {
170 		krb5_klog_syslog(LOG_ERR,
171 			gettext("Change password request failed "
172 					"Failed to build kadmin/changepw "
173 					"principal: %s"),
174 			error_message(ret));
175 		numresult = KRB5_KPASSWD_HARDERROR;
176 		(void) strlcpy(strresult,
177 				"Failed building kadmin/changepw principal",
178 				sizeof (strresult));
179 		goto chpwfail;
180 	}
181 
182 	ret = krb5_rd_req(context, &auth_context, &ap_req, changepw, keytab,
183 			NULL, &ticket);
184 
185 	if (ret) {
186 		char kt_name[MAX_KEYTAB_NAME_LEN];
187 		if (krb5_kt_get_name(context, keytab,
188 				kt_name, sizeof (kt_name)))
189 			strncpy(kt_name, "default keytab", sizeof (kt_name));
190 
191 		switch (ret) {
192 		case KRB5_KT_NOTFOUND:
193 		krb5_klog_syslog(LOG_ERR,
194 			gettext("Change password request failed because "
195 					"keytab entry \"kadmin/changepw\" "
196 					"is missing from \"%s\""),
197 			kt_name);
198 		break;
199 		case ENOENT:
200 		krb5_klog_syslog(LOG_ERR,
201 			gettext("Change password request failed because "
202 					"keytab file \"%s\" does not exist"),
203 			kt_name);
204 		break;
205 		default:
206 		krb5_klog_syslog(LOG_ERR,
207 			gettext("Change password request failed. "
208 				"Failed to parse Kerberos AP_REQ message: %s"),
209 			error_message(ret));
210 		}
211 
212 		numresult = KRB5_KPASSWD_AUTHERROR;
213 		(void) strlcpy(strresult, "Failed reading application request",
214 					sizeof (strresult));
215 		goto chpwfail;
216 	}
217 
218 	/*
219 	 * Set up address info
220 	 */
221 	addrlen = sizeof (local_addr);
222 
223 	if (getsockname(s, &local_addr, &addrlen) < 0) {
224 		ret = errno;
225 		numresult = KRB5_KPASSWD_HARDERROR;
226 		(void) strlcpy(strresult,
227 				"Failed getting server internet address",
228 				sizeof (strresult));
229 		goto chpwfail;
230 	}
231 
232 	/*
233 	 * Some brain-dead OS's don't return useful information from
234 	 * the getsockname call.  Namely, windows and solaris.
235 	 */
236 	if (((struct sockaddr_in *)&local_addr)->sin_addr.s_addr != 0) {
237 		local_kaddr.addrtype = ADDRTYPE_INET;
238 		local_kaddr.length = sizeof (((struct sockaddr_in *)
239 						&local_addr)->sin_addr);
240 		/* CSTYLED */
241 		local_kaddr.contents = (krb5_octet *) &(((struct sockaddr_in *)&local_addr)->sin_addr);
242 	} else {
243 		krb5_address **addrs;
244 
245 		krb5_os_localaddr(context, &addrs);
246 
247 		local_kaddr.magic = addrs[0]->magic;
248 		local_kaddr.addrtype = addrs[0]->addrtype;
249 		local_kaddr.length = addrs[0]->length;
250 		if ((local_kaddr.contents = malloc(addrs[0]->length)) == 0) {
251 			ret = errno;
252 			numresult = KRB5_KPASSWD_HARDERROR;
253 			(void) strlcpy(strresult,
254 				"Malloc failed for local_kaddr",
255 				sizeof (strresult));
256 			goto chpwfail;
257 		}
258 
259 		(void) memcpy(local_kaddr.contents, addrs[0]->contents,
260 				addrs[0]->length);
261 		allocated_mem++;
262 
263 		krb5_free_addresses(context, addrs);
264 	}
265 
266 	addrlen = sizeof (remote_addr);
267 
268 	if (getpeername(s, &remote_addr, &addrlen) < 0) {
269 		ret = errno;
270 		numresult = KRB5_KPASSWD_HARDERROR;
271 		(void) strlcpy(strresult,
272 				"Failed getting client internet address",
273 				sizeof (strresult));
274 		goto chpwfail;
275 	}
276 
277 	remote_kaddr.addrtype = ADDRTYPE_INET;
278 	remote_kaddr.length = sizeof (((struct sockaddr_in *)
279 					&remote_addr)->sin_addr);
280 	/* CSTYLED */
281 	remote_kaddr.contents = (krb5_octet *) &(((struct sockaddr_in *)&remote_addr)->sin_addr);
282 	remote_kaddr.addrtype = ADDRTYPE_INET;
283 	remote_kaddr.length = sizeof (sin->sin_addr);
284 	remote_kaddr.contents = (krb5_octet *) &sin->sin_addr;
285 
286 	/*
287 	 * mk_priv requires that the local address be set.
288 	 * getsockname is used for this.  rd_priv requires that the
289 	 * remote address be set.  recvfrom is used for this.  If
290 	 * rd_priv is given a local address, and the message has the
291 	 * recipient addr in it, this will be checked.  However, there
292 	 * is simply no way to know ahead of time what address the
293 	 * message will be delivered *to*.  Therefore, it is important
294 	 * that either no recipient address is in the messages when
295 	 * mk_priv is called, or that no local address is passed to
296 	 * rd_priv.  Both is a better idea, and I have done that.  In
297 	 * summary, when mk_priv is called, *only* a local address is
298 	 * specified.  when rd_priv is called, *only* a remote address
299 	 * is specified.  Are we having fun yet?
300 	 */
301 	if (ret = krb5_auth_con_setaddrs(context, auth_context, NULL,
302 					&remote_kaddr)) {
303 		numresult = KRB5_KPASSWD_HARDERROR;
304 		(void) strlcpy(strresult,
305 				"Failed storing client internet address",
306 				sizeof (strresult));
307 		goto chpwfail;
308 	}
309 
310 	/*
311 	 * Verify that this is an AS_REQ ticket
312 	 */
313 	if (!(ticket->enc_part2->flags & TKT_FLG_INITIAL)) {
314 		numresult = KRB5_KPASSWD_AUTHERROR;
315 		(void) strlcpy(strresult,
316 				"Ticket must be derived from a password",
317 				sizeof (strresult));
318 		goto chpwfail;
319 	}
320 
321 	/*
322 	 * Construct the ap-rep
323 	 */
324 	if (ret = krb5_mk_rep(context, auth_context, &ap_rep)) {
325 		numresult = KRB5_KPASSWD_AUTHERROR;
326 		(void) strlcpy(strresult,
327 				"Failed replying to application request",
328 				sizeof (strresult));
329 		goto chpwfail;
330 	}
331 
332 	/*
333 	 * Decrypt the new password
334 	 */
335 	cipher.length = (req->data + req->length) - ptr;
336 	cipher.data = ptr;
337 
338 	if (ret = krb5_rd_priv(context, auth_context, &cipher,
339 				&clear, &replay)) {
340 		numresult = KRB5_KPASSWD_HARDERROR;
341 		(void) strlcpy(strresult, "Failed decrypting request",
342 					sizeof (strresult));
343 		goto chpwfail;
344 	}
345 
346 	ret = krb5_unparse_name(context, ticket->enc_part2->client, &clientstr);
347 	if (ret) {
348 		numresult = KRB5_KPASSWD_HARDERROR;
349 		(void) strcpy(strresult, "Failed decrypting request");
350 		goto chpwfail;
351 	}
352 
353 	/*
354 	 * Change the password
355 	 */
356 	if ((ptr = (char *)malloc(clear.length + 1)) == NULL) {
357 		ret = errno;
358 		numresult = KRB5_KPASSWD_HARDERROR;
359 		(void) strlcpy(strresult, "Malloc failed for ptr",
360 			sizeof (strresult));
361 		krb5_free_unparsed_name(context, clientstr);
362 		goto chpwfail;
363 	}
364 	(void) memcpy(ptr, clear.data, clear.length);
365 	ptr[clear.length] = '\0';
366 
367 	ret = (kadm5_ret_t)kadm5_chpass_principal_util(server_handle,
368 						ticket->enc_part2->client,
369 						ptr, NULL, strresult,
370 						sizeof (strresult));
371 
372 	/*
373 	 * Zap the password
374 	 */
375 	(void) memset(clear.data, 0, clear.length);
376 	(void) memset(ptr, 0, clear.length);
377 	if (clear.data != NULL) {
378 		krb5_xfree(clear.data);
379 		clear.data = NULL;
380 	}
381 	free(ptr);
382 	clear.length = 0;
383 
384 	clen = strlen(clientstr);
385 	trunc_name(&clen, &cdots);
386 	krb5_klog_syslog(LOG_NOTICE, "chpw request from %s for %.*s%s: %s",
387 		inet_ntoa(((struct sockaddr_in *)&remote_addr)->sin_addr),
388 		clen, clientstr, cdots, ret ? error_message(ret) : "success");
389 	krb5_free_unparsed_name(context, clientstr);
390 
391 	if (ret) {
392 		if ((ret != KADM5_PASS_Q_TOOSHORT) &&
393 		    (ret != KADM5_PASS_REUSE) &&
394 		    (ret != KADM5_PASS_Q_CLASS) &&
395 		    (ret != KADM5_PASS_Q_DICT) &&
396 		    (ret != KADM5_PASS_TOOSOON))
397 			numresult = KRB5_KPASSWD_HARDERROR;
398 		else
399 			numresult = KRB5_KPASSWD_SOFTERROR;
400 		/*
401 		 * strresult set by kadb5_chpass_principal_util()
402 		 */
403 		goto chpwfail;
404 	}
405 
406 	/*
407 	 * Success!
408 	 */
409 	numresult = KRB5_KPASSWD_SUCCESS;
410 	(void) strlcpy(strresult, "", sizeof (strresult));
411 
412 chpwfail:
413 
414 	clear.length = 2 + strlen(strresult);
415 	if (clear.data != NULL) {
416 		krb5_xfree(clear.data);
417 		clear.data = NULL;
418 	}
419 	if ((clear.data = (char *)malloc(clear.length)) == NULL) {
420 		ret = errno;
421 		numresult = KRB5_KPASSWD_HARDERROR;
422 		(void) strlcpy(strresult, "Malloc failed for clear.data",
423 			sizeof (strresult));
424 	}
425 
426 	ptr = clear.data;
427 
428 	*ptr++ = (numresult>>8) & 0xff;
429 	*ptr++ = numresult & 0xff;
430 
431 	(void) memcpy(ptr, strresult, strlen(strresult));
432 
433 	cipher.length = 0;
434 
435 	if (ap_rep.length) {
436 		if (ret = krb5_auth_con_setaddrs(context, auth_context,
437 					&local_kaddr, NULL)) {
438 		    numresult = KRB5_KPASSWD_HARDERROR;
439 		    (void) strlcpy(strresult,
440 			"Failed storing client and server internet addresses",
441 			sizeof (strresult));
442 		} else {
443 			if (ret = krb5_mk_priv(context, auth_context, &clear,
444 						&cipher, &replay)) {
445 				numresult = KRB5_KPASSWD_HARDERROR;
446 				(void) strlcpy(strresult,
447 					"Failed encrypting reply",
448 					sizeof (strresult));
449 			}
450 		}
451 	}
452 
453 	/*
454 	 * If no KRB-PRIV was constructed, then we need a KRB-ERROR.
455 	 * If this fails, just bail.  There's nothing else we can do.
456 	 */
457 	if (cipher.length == 0) {
458 		/*
459 		 * Clear out ap_rep now, so that it won't be inserted
460 		 * in the reply
461 		 */
462 		if (ap_rep.length) {
463 			if (ap_rep.data != NULL)
464 				krb5_xfree(ap_rep.data);
465 			ap_rep.data = NULL;
466 			ap_rep.length = 0;
467 		}
468 
469 		krberror.ctime = 0;
470 		krberror.cusec = 0;
471 		krberror.susec = 0;
472 		if (ret = krb5_timeofday(context, &krberror.stime))
473 			goto bailout;
474 
475 		/*
476 		 * This is really icky.  but it's what all the other callers
477 		 * to mk_error do.
478 		 */
479 		krberror.error = ret;
480 		krberror.error -= ERROR_TABLE_BASE_krb5;
481 		if (krberror.error < 0 || krberror.error > 128)
482 			krberror.error = KRB_ERR_GENERIC;
483 
484 		krberror.client = NULL;
485 		if (ret = krb5_build_principal(context, &krberror.server,
486 					    strlen(realm), realm,
487 					    "kadmin", "changepw", NULL)) {
488 			goto bailout;
489 		}
490 
491 		krberror.text.length = 0;
492 		krberror.e_data = clear;
493 
494 		ret = krb5_mk_error(context, &krberror, &cipher);
495 
496 		krb5_free_principal(context, krberror.server);
497 
498 		if (ret)
499 			goto bailout;
500 	}
501 
502 	/*
503 	 * Construct the reply
504 	 */
505 	rep->length = 6 + ap_rep.length + cipher.length;
506 	if ((rep->data = (char *)malloc(rep->length)) == NULL)  {
507 		ret = errno;
508 		goto bailout;
509 	}
510 	ptr = rep->data;
511 
512 	/*
513 	 * Length
514 	 */
515 	*ptr++ = (rep->length>>8) & 0xff;
516 	*ptr++ = rep->length & 0xff;
517 
518 	/*
519 	 * Version == 0x0001 big-endian
520 	 */
521 	*ptr++ = 0;
522 	*ptr++ = 1;
523 
524 	/*
525 	 * ap_rep length, big-endian
526 	 */
527 	*ptr++ = (ap_rep.length>>8) & 0xff;
528 	*ptr++ = ap_rep.length & 0xff;
529 
530 	/*
531 	 * ap-rep data
532 	 */
533 	if (ap_rep.length) {
534 		(void) memcpy(ptr, ap_rep.data, ap_rep.length);
535 		ptr += ap_rep.length;
536 	}
537 
538 	/*
539 	 * krb-priv or krb-error
540 	 */
541 	(void) memcpy(ptr, cipher.data, cipher.length);
542 
543 bailout:
544 	if (auth_context)
545 		krb5_auth_con_free(context, auth_context);
546 	if (changepw)
547 		krb5_free_principal(context, changepw);
548 	if (ap_rep.data != NULL)
549 		krb5_xfree(ap_rep.data);
550 	if (ticket)
551 		krb5_free_ticket(context, ticket);
552 	if (clear.data != NULL)
553 		krb5_xfree(clear.data);
554 	if (cipher.data != NULL)
555 		krb5_xfree(cipher.data);
556 	if (allocated_mem)
557 		krb5_xfree(local_kaddr.contents);
558 
559 	return (ret);
560 }
561 
562 
563 /*
564  * This routine is used to handle password-change requests received
565  * on kpasswd-port 464 from MIT/M$ clients.
566  */
567 void
568 handle_chpw(krb5_context context, int s1,
569 		void *serverhandle, kadm5_config_params *params)
570 {
571 	krb5_error_code ret;
572 	char req[MAXAPREQ];
573 	int len;
574 	struct sockaddr_in from;
575 	int fromlen;
576 	krb5_keytab kt;
577 	krb5_data reqdata, repdata;
578 	int s2 = -1;
579 
580 	reqdata.length = 0;
581 	reqdata.data = NULL;
582 	repdata.length = 0;
583 	repdata.data = NULL;
584 
585 	fromlen = sizeof (from);
586 
587 	if ((len = recvfrom(s1, req, sizeof (req), 0, (struct sockaddr *)&from,
588 			    &fromlen)) < 0) {
589 		krb5_klog_syslog(LOG_ERR, gettext("chpw: Couldn't receive "
590 				"request: %s"), error_message(errno));
591 		return;
592 	}
593 
594 	if ((ret = krb5_kt_resolve(context, params->admin_keytab, &kt))) {
595 		krb5_klog_syslog(LOG_ERR, gettext("chpw: Couldn't open "
596 				"admin keytab %s"), error_message(ret));
597 		return;
598 	}
599 
600 	reqdata.length = len;
601 	reqdata.data = req;
602 
603 	/*
604 	 * This is really obscure.  s1 is used for all communications.  it
605 	 * is left unconnected in case the server is multihomed and routes
606 	 * are asymmetric.  s2 is connected to resolve routes and get
607 	 * addresses.  this is the *only* way to get proper addresses for
608 	 * multihomed hosts if routing is asymmetric.
609 	 *
610 	 * A related problem in the server, but not the client, is that
611 	 * many os's have no way to disconnect a connected udp socket, so
612 	 * the s2 socket needs to be closed and recreated for each
613 	 * request.  The s1 socket must not be closed, or else queued
614 	 * requests will be lost.
615 	 *
616 	 * A "naive" client implementation (one socket, no connect,
617 	 * hostname resolution to get the local ip addr) will work and
618 	 * interoperate if the client is single-homed.
619 	 */
620 
621 	if ((s2 = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
622 		krb5_klog_syslog(LOG_ERR, gettext("chpw: Cannot create "
623 				"connecting socket: %s"), error_message(errno));
624 		goto cleanup;
625 	}
626 
627 	if (connect(s2, (struct sockaddr *)&from, sizeof (from)) < 0) {
628 		krb5_klog_syslog(LOG_ERR, gettext("chpw: Couldn't connect "
629 				"to client: %s"), error_message(errno));
630 		if (s2 > 0)
631 			(void) close(s2);
632 		goto cleanup;
633 	}
634 
635 	if ((ret = process_chpw_request(context, serverhandle,
636 					params->realm, s2, kt, &from,
637 					&reqdata, &repdata))) {
638 		krb5_klog_syslog(LOG_ERR, gettext("chpw: Error processing "
639 				"request: %s"), error_message(ret));
640 	}
641 
642 	if (s2 > 0)
643 		(void) close(s2);
644 
645 	if (repdata.length == 0 || repdata.data == NULL) {
646 		/*
647 		 * Just return.  This means something really bad happened
648 		 */
649 		goto cleanup;
650 	}
651 
652 	len = sendto(s1, repdata.data, repdata.length, 0,
653 		    (struct sockaddr *)&from, sizeof (from));
654 
655 	if (len < repdata.length) {
656 		krb5_xfree(repdata.data);
657 
658 		krb5_klog_syslog(LOG_ERR, gettext("chpw: Error sending reply:"
659 				" %s"), error_message(errno));
660 		goto cleanup;
661 	}
662 
663 	if (repdata.data != NULL)
664 		krb5_xfree(repdata.data);
665 cleanup:
666 	krb5_kt_close(context, kt);
667 }
668